Builder paths
For new hosts, build from a mob definition.definition_path loads TOML, while
definition accepts an already parsed MobDefinition:
definition(...) or
definition_path(...) is required. The other common fields have defaults:
The older
mob_spec(...) path accepts a pre-built MobBootstrapSpec. It still
requires explicit module configuration and timeout because the spec already
owns its session service and construction policy:
mob_spec is present, the builder returns
MissingRequiredField(MobSpec). On the legacy path, omitting
module_config or timeout returns the corresponding missing-field error.
Optional extension documents
Rust hosts can enable the default-offextension-state Cargo feature and
register an external ToolBundleFactory. The extension package owns its tools
and payload schema; Meta supplies durable storage and caller authority. The
following example uses a durable-documents-test factory with read and edit
tools. Profile selection keeps the tools off agents that do not need them:
connect_timeout_secs) plus at most 0.25 s (plus the
TERM grace for a server that ignores SIGTERM), and its first turn starts
after that. A member is constructed when the mob builds it. At the runtime’s
start the mob builds only the members member_restore names: under the
default on_demand, the members that hold work, owe a kickoff, have a
network transport endpoint or a host requirement; under eager, every
member. The start restores META_IDENTITY_RESTORE_CONCURRENCY members at a
time (16 by default, at most 16; 1 serializes them), so servers that never
answer add about that timeout times ceil(built members / concurrency) to it.
Every other member waits at its first wake. A failed connection completes
the wait without making its tools available, and the member’s session holds
a typed setup-failure notice for it.
The configured storage provider must support durable extension documents.
The disk provider supplies this capability. No factory means no extension
store or tool registration, even if the feature is compiled.
Documents start private to the executing agent. Forks inherit the owner’s
data access by default; ordinary spawned helpers need an explicit grant or
descendant sharing. Reader grants remain read-only through forks, and
inherited access never permits changing grants, transferring or deleting a
document. Revocation is checked on the next access. Native policy always
applies; an enabled host access controller additionally checks
extension.read, extension.edit and extension.manage. With host ABAC
disabled or unconfigured, that layer adds no restriction. See
Access control for host rules.
Document persistence does not change native member lifetime. In the current
Meta composition, delegate uses an ephemeral child mob and retires the
helper and its fork descendants when delegation completes. Such members
cannot be resumed after restart. Persistent root-mob members retain their
native identity and lineage through the configured durable mob storage.
The convenience persistent_state(...) path persists sessions, the native
mob journal (<persistent_state>/event_log.sqlite3) and the other stores, so
members keep their identity and lineage across process restarts. A host that
keeps the journal elsewhere supplies mob_spec(...) using
MobBootstrapSpec::persistent(...) with persistent_mob_storage(...) and its
storage provenance. Durable document rows alone cannot reconstruct missing
native creation facts; the extension refuses unavailable authority.
Documents name a mob by its id alone, as their owner (Owner::Mob) or in an
audience grant or deny (Audience::Mob), and they outlive the mob. A new mob
under the id of a destroyed one would inherit what they grant, so the agent
mob-tool state refuses every create (an agent’s mob_create, a temporary
council, a create without an owner session) under an id that any live
document of the realm still names, whatever is bound under the id. The typed
refusal names the remedies: choose another mob id, or have a host or
operator with access to the extension store transfer each document the id
owns to another owner and remove or replace each Audience::Mob grant or
deny naming it. An owner transfer alone leaves the audience entries, and the
document tools alone cannot manage a document whose owning mob was
destroyed.
The one exemption is the implicit delegation mob that its own session’s
runtime path creates or recreates (as for a model change). Ownership is by
id, not by mob instance, so the owner session of a destroyed mob cannot
recreate the same explicit id while documents name it either. The built-in
SQLite store answers the check from an index of the mob ids its live
documents name: it reads a realm’s documents once, at the first check after
it opens, keeps the index with its own writes, and reads the realm again
after another connection commits, so a check costs a lookup, not a scan of
the realm under the store’s one connection. A refusal tells the caller that
some document names the id, including one it cannot read.
The check is ordered against document operations. It waits for every
operation in flight through the document services the runtime builds (tool
bundles and document MCP bindings) and sees what they committed. The caller
resolver stamps each caller with the generation it read before any
authority (HostAccessContext::authority_generation), and an admitted
create under an id, or the release of the destroyed mob that held it, moves
that id’s generation. An operation is refused with AuthorityUnavailable
when a mob id its caller’s authority names
(HostAccessContext::authority_mob_ids), or a write’s new owner or access
names, changed after the stamp, and the tool resolves its caller again. The
document services refuse a caller context the resolver did not resolve,
such as one the host built itself; a host that needs unordered access uses
its provider’s store directly. The ordering covers one runtime in one
process, and the implicit delegation mob’s exemption above takes no part in
it.
Every activation checks who holds the mob’s id. The document store records,
per realm and mob id, the holder of the mob that took the id
(MobIdHolder): the owner session it was created for, or the host for a
mob created with no owner session. A create the check admitted (the
implicit delegation mob’s included) records its holder and binds.
Activations under an id are decided one at a time: one that finds a live mob
bound under the id is refused, records nothing and drops whatever admission
is pending under the id. A restore, or a held child’s resume, binds when the
store records the mob’s own holder for the id, or when no live document names
the id; an id with no
holder record (references from before 0.9.3, or a mob created without the
extension’s hooks) is adopted by the first mob restored under it, which
records its holder. When live documents name the id and the store records
another holder, the restored mob is quarantined, and so it is when the
store cannot answer. A quarantined mob is bound (an error would abort
Meerkat’s restore of every mob), but its members resolve no document
caller, it is no valid document owner, nobody manages it and lineage
through it does not resolve. UnifiedRuntime::extension_mob_quarantines
lists each one with its typed reason. It stays quarantined until it is
destroyed, or until a later restore finds that an operator cleared the
references. Holder records outlive a destroy, so restoring a whole state
generation (documents, holder records and mob state together) keeps its
mobs bound. Holders are owner sessions, and every mob the host creates
without one is the same holder, so a re-creation under an id by the same
session, or by the host, without the extension’s hooks is adopted as that
holder’s mob. An admission whose create failed after the check stays until
the id’s next admission or activation: a destroy under the id refreshes an
admission made after that destroy rather than expiring it, and the next
activation under the id that names the same holder binds without the holder
check.
A document store a host supplies through
MetaStorageProvider::open_extension_state must implement
ExtensionDocumentStore::references_mob, failing the answer (never skipping
the document) on a document it cannot read, and
ExtensionDocumentStore::mob_id_holder and
ExtensionDocumentStore::record_mob_id_holder, recording holders durably
beside the documents. The defaults answer errors: every create except a
session’s implicit delegation mob is then refused, and every mob is bound
quarantined. meta_store_conformance::chapters::mob_references and
mob_id_holders check an implementation. The built-in store keeps its
holders in a plain extension_mob_holders table beside its ledgered
tables, which Meta 0.9.2 ignores.
The extension resolves the identity behind a session through Core’s member
history, which keeps every binding Core committed for an identity, including
bindings from before the extension was enabled. A history Core cannot read
answers typed ExtensionAuthorityUnavailable; it is never interpreted as an
unrelated Worker. Disabling the factory does not remove stored documents.
The extension integration requires the Meerkat crate family at the exact
versions declared in the repository manifests. Applications using the Meta Git
dependency should pin the reviewed revision and resolve their committed
lockfile against those packages.
Keeping a Meta realm in your own database
A host that keeps its data in its own database can keep the whole Meta realm there, through Meerkat Core’s host store SPI: the host implements Core’sHostStore trait over its database (see Core’s host store reference), and
HostStoreMetaStorageProvider puts the realm on it. The provider opens the
realm’s one SpiRealm (the fence, its lease and every scope) and hands that
one handle to every store the runtime composes. Nothing else in the process
opens the host store.
The local directory is a per-host cache:
scratch_dir() when set, else a
temporary directory the runtime owns. It holds the agent factory’s files and
engine state only.
Comms identities. Each member’s comms identity (its key pair, one
directory per session) is in neither the realm nor the cache: it lives under
the session comms identity root, by default
$XDG_STATE_HOME/meerkat/session_comms_identity on Linux (else
~/.local/state/meerkat/session_comms_identity) and
~/Library/Application Support/meerkat/session_comms_identity on macOS. A
restored member’s comms endpoint must match the one the realm recorded for
it, so a start without that directory brings every restored member up broken,
and the storage census (comms_identity) and the startup log name the
directory, its path and the members. Keep it on storage that survives a
restart and moves with the realm, such as a persistent volume, and point the
runtime at it with the builder’s session_identity_config_root(root), which
keeps the keys under <root>/.rkat/session_comms_identity, as the gateway’s
runtime_options.session_identity_config_root does. With that, a start on
another host with an empty cache recovers exactly as a restart on the same
host does. The first start after an import needs no keys: each imported
member gets a new key pair. Turning the setting on, or changing it, for
members whose keys already live elsewhere marks every one of them broken at
the next start: copy their per-session key directories into
<root>/.rkat/session_comms_identity first, or set the root from the first
start after an import, as for a cutover. The same root is also the agent
factory’s user-global convention root, which Core consults when it resolves
skill repositories, as with the gateway’s setting. An empty root is refused.
Open the provider before the build. A second process on the same realm
meets HostStoreOpenError::Held, with what remains of the holder’s lease;
wait it out and open again, or open with AcquireMode::Takeover once the
holder is known to be gone:
provider.readiness() from your
readiness probe: is_ready() is false while the realm or a scope is
poisoned, and after a start that found restored members without their comms
identity on this host (comms_identity_degraded) or members Core restored
broken (broken_members): those members serve nothing until they are
repaired.
Restart wait. The SPI has no fence release: after a stop, the next open
waits at most the lease the host declared in its FenceTiming (30 seconds by
default). A shorter lease shortens the wait.
Event log. The builder’s event_log(config) works over a host store
realm when HostStoreMetaStores::event_log_store is None: the builder’s
config, store and settings are used. A store in event_log_store and no
event_log() ingests with the default settings. Supplying both refuses with
ConflictingConfiguration, naming the event log.
Cost of a round. A host store realm keeps each session’s document whole
(WholeBlob): every round commit writes the whole document, so a tool round
writes about twice its size, and a long tool-heavy run writes bytes that grow
with the square of its rounds. Bound the document size with the builder’s
compaction() policy.
Limits on a host store realm. The storage census (meta/status,
meta/capabilities) lists the process-bound and per-host slots:
- Agent-created child mobs (
mob_create, delegate’s implicit mob) and temporary councils are process-bound: they work, and they do not survive a restart. A durable child mob storage is refused. Members of the realm’s root mob do survive a restart, including the ones agents spawn into it: a long-lived member an agent creates on a host store realm is spawned into its own mob (mob_spawn_memberor the member toolspawn_member, from adurable = trueprofile), not into a child mob (see Long-lived members an agent creates). - The builtin task tools and the file memory tool keep their files under the agent factory’s store path, the per-host cache.
- Each member’s comms key pair is in the per-host comms identity directory (see Comms identities above).
- An OAuth login in progress does not survive a restart (completed credentials do).
persistent_state(),mob_storage(),continuity_from_state_dir()and the per-slot store seams the provider supplies are refused, as are atopology_control()mode other than Disabled (aTopologyProvider’s desired topology works: it is reconciled against the mob’s wiring, which the realm keeps),persistent_agent_memory_stack()without a provider memory, and extension documents.- One root mob per realm, in the mob storage named
root; the mob keeps the id its definition gives it. - The builder is the only entry:
meta_gatewayandrpc_gatewaydo not open a host store in this release.
mob_announcements() works over a host store realm:
the announcement store keeps its tables in the realm, and the storage census
lists mob_announcements there. The process works on an in-memory copy of
the tables, loaded whole at the start, and every call that changes the store
is one batch of the realm, settled before the call answers. A collector
step’s items, their captures, the cursor and the step’s receipt land together
or not at all.
- Your scheduler runs the collectors. Meta schedules no source runner on
a host store realm. A declared source with a runner is accepted, and
sources/getreportsrunner_scheduled_here: false. Your own scheduler fires each source’s collector at itsrunner.every_seconds, read fromruntime.mob_announcements()?.sources(...), and hands each step toadvance_sourcewith a new operation id. A retry of a step reuses its operation id, and its original result answers again, marked replayed. - Latency. Calls run one at a time, and reads wait behind a commit in flight, so the store takes about one change per host commit latency. A collector step of up to 100 items with all its captures is one commit; a delivered batch takes a few commits as it moves from sealed to handled. A handling read that finds the turn still pending and an escalation pass that only moves its mark wait in the copy for the next commit, or a flush within 60 seconds. Nothing flushes them at shutdown, so a graceful stop loses them just as a crash does, and at most them: a lost read falls due at once at the next start.
- Starts and rewrites. The call that takes the store’s journal to 400 rows writes the store whole again, in one batch (and its chunks first), and waits for it; a start reads that image and fewer than 400 rows after it. The whole store is held in memory while the process runs, and while a rewrite runs the process holds about three more copies of it (the image, its encoded rows and the packed chunk).
- The start read.
build()waits for the store’s start read, which runs after the identity bootstrap: with entries of up to 256 KiB it takes at most 27 read calls of your host store, plus one per 16 MiB of the store’s image. Until it is done, the member tools answer that mob announcements are not ready on this gateway. - Failures. A batch the host refuses, or whose outcome stays unknown,
poisons the announcement store: every later call refuses, and
provider.readiness()turns false, until the process opens the realm again. - The lease. Every announcement call, reads included, first makes sure the realm’s lease is live. While the lease can be neither renewed nor taken again (a host outage longer than the self-fence window), announcement calls refuse, reads included; they serve again on their own once a renew or a re-acquire succeeds.
- No import. Announcement state in a state directory is not moved into a realm. A new realm’s store starts empty: declarations make their sources and subscriptions again, and each source onboards from no cursor.
storage_migrate_native::import_continuity_export_into_host_store. A dry run
reports each session and writes no session, document or row; an apply
creates each session’s document in the realm and the first-boot rows the
first start carries out, and a rerun with the same export continues an
import that stopped part way. Both take the realm’s fence like any open and
hold it until its lease lapses, so a runtime started right after either
waits up to that lease.
Until the import is done, opening the realm refuses with
HostStoreOpenError::ImportIncomplete. Moving a 0.9.4 or 0.9.5 state
directory into a host store realm is not covered in this release.
No downgrade. An earlier Meta, or a provider of your own, over a host
store realm ignores Meta’s records in it and would compose a different mob
storage: do not run one on a realm this release wrote.
A storage_provider() whose backend keeps the sessions but runs over a
scratch_dir() keeps its mob storage in memory unless mob_storage()
supplies one, so after a restart every identity starts on a fresh session
while its old one remains. The storage census and the log warn about it; keep
such a realm in a host store instead.
Bootstrap sequence
1
Mob preparation
The runtime first prepares the Meerkat mob. This does not unconditionally provision members or finish wiring. A persistent identity-first resume can remain staged in
Stopped until activation can proceed. A mob preparation failure returns UnifiedRuntimeBootstrapError::Mob.2
Module startup
After mob preparation, the supervisor attempts each discovered module and records its startup transitions. A module that exhausts its restart policy remains unloaded and emits a supervisor warning. A module-runtime initialization error or startup-thread panic rolls back the prepared mob before the builder returns an error.
3
Configured member materialization and wiring
Later in builder construction, configured identity roster bootstrap or classic
Discovery spawning materializes the desired members. The builder then performs final edge reconciliation when configured. Modules must not assume an already-populated, fully wired, Running mob at startup.4
Event merging
Agent and module events feed the unified event transport. Hosts read the public snapshot and subscription APIs rather than accessing the internal event vector.
Discovery can have no members; hosts must
declare or explicitly create them. Persistent resumes can already have
members, so this is not a guarantee that every bootstrap starts empty. See
Roster and member lifecycle.
Rollback on failure
If module-runtime initialization fails or its startup thread panics after mob preparation, the runtime performs a coordinated rollback:- Attempt to shut down the mob runtime
- If rollback succeeds, return the original module startup error
- If rollback also fails, return
ModuleStartupRollbackFailedcontaining both errors
Runtime handle
After successful bootstrap, the builder returns aUnifiedRuntime handle with access to:
The raw
MetaRuntimeHandle is intentionally not exposed. Module operations
such as routing, delivery, memory, and gating are methods on UnifiedRuntime.
Reconciliation
The reconciliation method depends on which authority model built the runtime. For a classic runtime without identity-first authority, pass the complete desired member specs:desired_specs is a Vec<SpawnMemberSpec>. This path reconciles members,
managed peer edges, and delivery routing. When the router module is loaded,
it maintains route keys named mob.member.{member_id} with the
notification channel.
For an identity-first runtime, roster and topology providers own desired state.
Refresh those providers through the identity authority instead:
refresh_desired_topology returns Some(RestoreFlowResult) when identity-first
authority is attached and None otherwise. Calling whole-mob reconcile on an
identity-first runtime fails rather than bypassing that authority.
Retiring an identity at a captured scope
retire_member_alias_tracked refuses a generated alias of an older
generation, but Core can move a member’s session binding without a new
generation: an alias captured before such a move still retires the member on
its new session. A host that must retire exactly the member it captured
retires at its saved DeliveryScope instead (see
scope-bound dispatch):
retire is. Under the identity’s lifecycle
lock, Meta reads Core’s live member scope and compares it with the saved
one. When the scope names the live member, Core releases that member’s live
materialization: the identity stays a member of Core’s mob, on the same
session at the same generation, so the saved scope, and every scope saved for
that incarnation, stays valid, and the next delivery materializes the member
again. A release is no retirement, so no memory hook runs (no distillation
of the session, no exit-interview note), and a release Core refuses changes
nothing. Nothing else is touched: no spawned_by descendant of the member is
retired (earlier releases retired them with retire’s process-tree
semantics), and a successor minted after the capture (a reset, a delete, a
respawn) is never released through the old scope. The host’s retirement hook
hears of the registration the release ended, with the unchanged generation.
Meta holds no lease for the call and writes no continuity record; scoped
dispatch compares the same Core facts.
It returns the Core fence of the incarnation the scope names. Ok also
answers a scope no incarnation of the identity is current at, and a call for
an identity Core holds no member for, releasing nothing, so a retry after a
lost reply converges to Ok.
A refusal releases nothing:
Unsupported: the bridge has no scoped seam, or the identity is externally bound.Rejected: a scope of another identity, or one wrapped around another identity’s member; a runtime that no longer admits identity operations (it is shutting down, or its mob is closed); or Core’s live member scope could not be read.Refused(IdentityHostAdopted): Core holds an identity intent row for the identity, so the host owns it.Refused(IdentityHistoryUnavailable): Core cannot read that row.StaleScope { mismatch }: another incarnation of the identity is current, so the scope is stale for good; capture it again.RuntimeId: it runs under another runtime id (after a reset, a delete or a respawn).LeaseFencingToken: the runtime id is the same but Core’s fence or the scope’s binding moved; the slot keeps its 0.9.3 name and carries Core’s fence.
Retire is Core’s release failing. Retire(IdentityWakeRetryable) means
Core does not release the member now because it is being woken or has work
in flight: nothing was interrupted, and the same call succeeds once the
member is quiescent. Any other Retire error is a failed release, or the
tracked operation ending without a result; read the identity’s status to see
where it stands.
Adding one held member to a running roster
bootstrap_roster_held holds a whole fresh roster at startup. To add one
fresh member while the others keep running, hold it the same way with
add_member_held on the identity-first context:
ExistingContinuity), and a failed read of that history is never taken as
fresh. An identity a host adopted (Core holds an identity intent row for it)
is refused (IdentityHostAdopted). A member embodied outside the add after
its freshness read refuses the add (NotHeld). In each case the host is
never asked. In lazy mode, too, the add embodies its member at once.
Before the member exists, the add marks its identity as owing the host a
held completion (in <state>/held-completion-owed/, as a held roster pass
does), and it clears the mark when it publishes the member. A member whose
add aborted never got a Core binding, so it can be added again. Until a held
pass (that re-add, or a held roster pass) publishes it, every other wake or
reset refuses it (HeldCompletionOwed), across a restart too; a delete finds
nothing to delete, since Core never spawned it, and the mark stays. On the
wire this refusal keeps the internal-error code -32603 with structured data
{kind: "held_completion_owed", identity}, in an RPC error and in a refused
meta/reconcile_identity outcome alike. After an eager roster pass the
identity’s bootstrap status entry is broken, with restore
{stage: "refused", code: -32603, kind: "held_completion_owed"}. Read it as
an admission still owed to the host, not as a failed member: only a held
pass that publishes the member clears it.
List the member from inside the completion, as above, never before the add.
Until the add ends it holds the bootstrap controller, so no pass of the
attached identity-first context reads the listing before the member is held
and completed. A spec listed before the add starts can be read by a pass
the host did not start, such as a meta/reconcile_identity, before the add
has written its mark: in eager mode that pass embodies the member unheld, and
in lazy mode it registers the member and an on-demand dispatch materializes
it unheld. The add then refuses with ExistingContinuity, after the member
has run without the host’s completion. Once the add has ended without
publishing the member, every other wake refuses it (HeldCompletionOwed).
Three paths run outside the controller, and so outside that guarantee:
meta/reconcile_identity on an RPC context that is not attached to the
unified runtime, which reads the roster and restores it with no controller;
IdentityRuntime::restore_flow(roster, ...), which registers and wakes the
roster its caller gives with no controller (it retires nothing); and the
host’s own bootstrap_roster(roster), which applies the roster it is given:
a roster resolved before the add and applied after it retires the member.
When the add ends Unsettled, unlist the member, or add it again. Until a
held pass publishes it, a pass that reads the listing refuses it
(HeldCompletionOwed), and the re-add’s own held pass covers it, so a
listed, owed member can be added again without unlisting it first. A caller
that goes away while the completion runs after listing never sees
Unsettled: the add aborts and the member stays listed and owed, so the
host unlists it, or adds it again, on its recovery path.
The add publishes no bootstrap generation. Until the next roster pass,
identity_bootstrap_status does not list the added member, and its counts
and ready do not cover it; the add’s own result is its outcome.
Shutdown
In the mob/module teardown sequence, graceful shutdown quiesces and stops the mob while its router and module dependencies remain alive. It then closes the event router and shuts down the modules:- Flow runs: the mob stop settles active flow runs first, because meerkat’s Stop refuses while a flow run is active. It cancels each one, awaits its terminal on the mob event ledger, and then stops the mob.
- Bound: the stop is bounded by
MOB_STOP_FLOW_SETTLE_BUDGET. A run that does not end in time is reported asMobRuntimeError::MobStopFlowRunsUnsettled, naming the runs and the last stop refusal. - Other refusals: any other refusal is returned as is. Teardown never re-sends the stop on a timer and never cancels member work, because meerkat’s Stop settles member runs itself.
shutdown returns only once the root mob’s actor task has returned (within
the root mob’s terminal shutdown budget), so after a teardown that settled
every member, dropping the runtime releases Core’s custody of the state
directory at once, and a new runtime composed on it in the same process is
not refused as in use (StorageLayoutError::RuntimeInUse).
HTTP composition and binding
build_reference_app_router is the public composition root for the health,
console, Flow Editor, JSON-RPC, blob, and SSE surfaces. The router does not bind
a socket by itself:
GatewayHttpBinding::bind_loopback selects an available port on 127.0.0.1.
The shipping gateways return that concrete http_base_url during
initialization. Embedders may instead serve the returned Axum router on their
own listener. A host-owned Axum server must keep event_drain_task running so
mob events reach console SSE and event-log consumers. During ordered shutdown,
drain http_server, abort the event-drain task, and then shut down the runtime.
Key routes in the reference app include:
GET /console/modules lists IDs from RuntimeDecisionState.modules.
It is a configuration inventory, not confirmation of successful startup,
current liveness, or health. Startup results are available separately through
loaded_modules().await and module_health_transitions().await.
Console voice and live from the builder
A library embedder that fronts the console with its own authentication (for example Google sign-in in front of Meta) can register the same live doors the gateway binaries expose, without building a session service by hand:- Persistent sessions (
persistent_state(...)) are required. Registering a live door on an ephemeral runtime failsbuild()closed withUnifiedRuntimeBuilderError::LiveCompose(LiveRequiresPersistentSessions);live(...)with an emptypublic_base_urlfails withLiveRequiresPublicBaseUrl. Console voice alone needs no URL: its transport is negotiated over WebRTC. - The doors compose once, after the runtime is shared:
UnifiedRuntime::compose_livebuilds one live context (one adapter host, one WebSocket state, one voice-path arbiter) and, over it, the console voice controller and the externalmeta/live/*handler. A success is bound once; an error is not cached, so a failing registration is retried on the next call. With both registered, they take turns on the voice path (“latest engaged wins”) exactly as in the gateway binaries; see the console guide for the typed reasons. build_reference_app_router_with_livecomposes, layers the controller the console RPC reads, and merges the live WebSocket router whenlive(...)was registered. It refuses withLiveComposeError::ConsoleVoiceRequiresAppAuthwhen voice is composed butconsole.require_app_authis false. The plainbuild_reference_app_routerkeeps working unchanged for hosts without live doors.- Accessors after composition:
console_voice_controller(),live_context(), andlive_rpc_handler()(dispatchmeta/live/*from your own transport, for example an embodiment daemon).
openai-live feature: link
the system Opus library so the build never falls back to compiling bundled Opus
through CMake (see the deployment guide), and expect the browser transport to
be WebRTC, so the console needs HTTPS or localhost.
Error hook and logging
Failures the runtime detects after bootstrap (a member spawn that failed, a checkpoint that did not persist, a stalled actor loop) have no caller to return to. They are emitted as typedErrorEvents through the error hook, registered
with UnifiedRuntimeBuilder::on_error or, for hosts that construct the runtime
through UnifiedRuntime::bootstrap, with UnifiedRuntime::set_error_hook.
The hook is fire-and-forget: it runs on a detached task, and a slow or failing
hook cannot break the runtime. An attached identity-first runtime receives the
same hook.
ErrorEvent is #[non_exhaustive] and serializes with an internal
category tag in snake_case, so match with a wildcard arm and branch on
category on the wire. The variants and where the runtime fires them:
actor_loop_terminated is terminal, not an actor_loop_recovered event.
It can occur without a prior stall (stall_id is then absent). The probe
ends, guarded deliveries fail fast with ActorTerminated, and there is no
in-process actor recovery. The operator must restart the process rather than
wait for a recovery notification.
Default sink (since 0.8.18). Every ErrorEvent is also logged through
tracing whether or not a hook is registered: at ERROR, or at INFO for
actor_loop_recovered and mob_announcement_escalation_resolved, with a
hook_registered field. lifecycle_effect_failed and
lifecycle_projection_off are failures and log at ERROR. A host that never
registers a hook additionally gets one WARN, no error hook is registered, so runtime error events reach logs only; register one with UnifiedRuntimeBuilder::on_error (or UnifiedRuntime::set_error_hook) to route them to paging: build() emits it at startup when on_error was never
called, and the fire path emits it once per process for hosts that bootstrap
directly and install no hook.
Gateways. rpc_gateway installs a hook that forwards every event to the
SDK as the meta/on_error JSON-RPC notification (params are the serialized
event); register a receiver with Python .on_error(callback) or TypeScript
.onError(callback). Because that forwarding hook is always installed, the
absent-hook WARN never fires for SDK hosts; an SDK host that registers no
callback sees only the gateway’s stderr ERROR line, which the SDKs inherit by
default (see Gateway logging).
meta_gateway installs no hook and relies on the log sink alone. Both
binaries install a stderr tracing subscriber whose default filter is
warn,meerkat_meta=info,<binary>=info; RUST_LOG overrides it.
Rust library hosts must install a tracing subscriber before build().
meerkat-meta emits tracing events and installs no subscriber of its own.
The default sink and the absent-hook notice are both tracing lines, so a host
without a subscriber sees neither the failures nor the hint that nobody is
listening. The minimum is a stderr subscriber honouring RUST_LOG:
Error hierarchy
See also
- Quickstart - minimal bootstrap example
- Modules - module configuration
- Console guide - admin console details
- Architecture - internal design
