MetaConfig
Top-level configuration for the module runtime.ModuleConfig
Raw Rust construction and serde deserialization of
ModuleConfig require all
four fields. The trust-manifest loader supplies
[] for omitted args and OnFailure for omitted restart_policy; those
adapter defaults are not defaults on ModuleConfig itself.
RestartPolicy
DiscoverySpec
PreSpawnData
SDK and Gateway runtime options
The stock SDK gateway (rpc_gateway) consumes the runtime_options object of meta/init. The Python and TypeScript builders emit it from their setters; where a row names a setter, that is the only way the SDK reaches the key, and a key with no setter on an SDK can be sent only from a hand-built meta/init payload. meta_gateway refuses a runtime_options object rather than dropping it.
Unsupported nested fields fail initialization with
-32602 instead of being silently ignored.
The gateway’s --persistent flag selects a long-lived stdio/process protocol;
it does not itself configure durable storage. The separate top-level
persistent_state path selects persistent storage composition.
Implicit delegate idle retirement
Idle retirement applies to members of implicit delegation mobs and to explicitly opted-in primary-mob members, not unconditionally to the durable identity roster. A primary-mob member becomes eligible through a per-delegate override or theimplicit_delegate_idle_retire_secs member label.
Per-member overrides take precedence: they can disable retirement, select a
numeric timeout, or use the runtime default. Without such an override, the
member label accepts a numeric seconds string or "disabled"; otherwise the
runtime default applies. Setting the runtime default to null therefore does
not disable a member’s explicit numeric timeout. See
roster and member lifecycle for the identity/worker boundary.
A member is idle while its session runs no turn. Its window starts when the
sweep first finds it idle under its current session, so a member re-seated or
woken under a new session starts a fresh window, and the sweep reads the
member’s session again right before it retires the member. A member the
previous pass did not find idle starts a fresh window too. When the
member’s runtime reports its runs, so does a member that ran a turn
since the sweep last found it idle, even a turn the sweep never saw
(its session’s last finished run changed); a session that answers no
execution snapshot reads idle. A member every pass finds idle, under
the same session and with no new run, keeps its window however long the
passes take. After a restart, a member whose session no live runtime serves yet
counts as idle while no kickoff of it is in flight, and it is retired only when
it owes no work: no input of its session short of a terminal state either. The
sweep reads that from the store, together with whether this process holds the
member’s session at all, only once the member’s window has passed; until then
it reads nothing from the store for the member. A store read that fails, or
does not answer within a minute, keeps the member for a later window and is
logged at WARN. The sweep never retires a member whose session this process
does not hold, such as one placed on another host, or one that still owes
work: such a member starts a fresh window.
Agents set a per-member override with the idle_retire_secs argument of the
delegate, fork_off and mob_spawn_member tools. A non-negative integer is
that many idle seconds. JSON null is the same as omitting the argument, like
every other optional tool argument:
Any other value is refused as invalid arguments. Agents cannot disable
retirement for the members they spawn; a host that wants helpers kept sets the
runtime default to
null (above). A disabled override persisted by an
earlier Meta, when idle_retire_secs: null meant disable, still loads and
still keeps its member.
Model fallback
Automatic model fallback is off by default. An enabled policy requires an explicit, non-empty chain; an empty chain never walks the model catalog. Meerkat owns retry eligibility, target admission, credentials, and committed model routing. Meta forwards that policy without adding its own retry loop. The host file selected bymeerkat_config_path accepts [model_fallback].
Rust embedders can pass the same meerkat::Config to
UnifiedRuntimeBuilder::meerkat_config. In mob.toml, use
[runtime.model_fallback] for a mob default or
[profiles.<name>.model_fallback] on an inline profile:
enabled = false wins over an
enabled parent. Tables are not merged field by field, so a profile does not
silently borrow its parent’s chain. A realm-profile reference cannot carry a
sibling fallback override; put it on the referenced profile or the mob runtime.
Unknown fallback keys, use_catalog_default_chain, invalid policy values, and
enabled = true without a chain are errors. There is no separate
runtime_options.model_fallback gateway option.
Cross-provider fallback requires explicit cross_provider = true and a usable
authorized credential binding; omission does not authorize switching accounts.
Targets must pass Meerkat’s context, output-budget, tool and modality admission.
Transport failures and empty output are not default fallback triggers.
New fallback provenance can cause an unsafe restored route to be held with a
typed error. Historical sessions without that provenance are not inferred
to be fallback sessions, automatically reverted, or migrated. Existing explicit
operator model/provider resume masks retain their authority.
The host config setter affects builder-created member session services, not an
already-built .mob_spec() service. Native background memory engines currently
use their own default config (also fallback-off); this table does not configure
those engines. This release does not add fallback overlays, automatic reversion,
or a routing-repair CLI.
One-shot mob definition update
Python hosts use the supported builder ceremony on the single activation authorized to change the durable definition:declareSpecUpdate():
mob_config and emits:
declare_spec_update(...)
(TypeScript declareSpecUpdate(...)) from later ordinary restarts. A stale
revision or different successor definition raises StorageResolutionError in
both SDKs; an exact retry returns the already
committed successor revision and converges its checked projection.
This is a cold-activation ceremony, not a hot reload. Fully shut down the prior
runtime before issuing the update: an already-running MobHandle keeps the
definition it was constructed with. The CAS and verified-resume fence serialize
the update with the next bootstrap, but they do not reconfigure an older live
process.
Self-hosted models and the Meerkat host config
Meerkat keeps three tables out of the mob definition and in the hostconfig.toml: [self_hosted] (serving endpoints and model aliases), [realm]
(backend, auth and binding profiles, including the credential binding every
provider = "self_hosted" member resolves through) and [models] (custom
model rows). A mob.toml profile refers to them by name only:
mob.toml
config.toml
[models.<id>] provider = "self_hosted"
is refused by Meerkat’s model registry: self-hosted aliases live under
[self_hosted.models] only.
[self_hosted] and [realm] belong in the host config, never in mob.toml.
MobDefinition::from_toml deserializes without deny_unknown_fields, so a
top-level [self_hosted] or [realm] table written into mob.toml used to
vanish without a diagnostic and resurface two layers later as a member that
could not build. Both binaries now refuse it at init, on the raw TOML text,
with one message:
-32602 on the meta/init request id
(meta_gateway when it loads config/mob.toml).
[models.<id>] is different from the two tables above. A profile’s model
must be catalogued, provider-annotated, or defined under the mob
definition’s own [models.<id>] to pass the model check: the check
(meerkat_mob::validate_definition, run at init on rpc_gateway and by
Meerkat at create time on both binaries) reads only the definition’s tables.
So a custom model a profile names by id belongs in mob.toml. A [models]
table in the host config still reaches the factory through
meerkat_config_path (a provider-annotated profile can use such a row), but
it does not satisfy the check, and a profile whose model exists only there and
carries no provider is refused at init as UnknownModel.
How the host config reaches each surface
The loader reads exactly the named file and merges it over
Config::default()
with Config::merge_toml_str, the step Meerkat applies to a discovered
.rkat/config.toml: no directory walk and no home-directory fallback, so what
the gateway loads is what the operator named. Adoption is never silent:
meta_gateway logs one INFO line naming the adopted file and whether the
init param or the workspace convention produced it. A file that cannot be read or
does not parse or does not validate refuses init (rpc_gateway: -32602,
runtime_options.meerkat_config_path: failed to read meerkat config <path>: ...,
meerkat config <path> is invalid: ... or
meerkat config <path> does not validate: ...; meta_gateway: -32602 on
the request id, init params: meerkat_config_path: ...), because an operator
who pointed the gateway at a file expects that file in force. Validation is
Meerkat’s own Config::validate against the canonical catalog, the step
rkat and rkat-rpc run after every load: a [self_hosted.models.<id>]
alias whose server is not declared, a [models.<id>] row colliding with a
catalog id, or a zero compaction threshold is refused at init instead of
killing the first member build. The gateway’s own
declarations stay layered on top: member_comms_address and compaction on
rpc_gateway, compaction on meta_gateway, and a profile’s own
auto_compact_threshold still wins per member.
The init model check
rpc_gateway checks every inline profile’s model before the mob exists, by
Meerkat’s own rule (meerkat_mob::validate_definition, narrowed to its
UnknownModel diagnostic): a model is acceptable when it is catalogued, or
defined under this definition’s [models.<id>], or provider-annotated
(provider = "self_hosted" with a self_hosted_server_id, for one). This is
the rule rkat mob validate documents and MobBuilder::create applies at
bootstrap. A refused model is -32602 with the profile named and, when a
catalog id is close, a Did you mean one of: ...? hint (the common cause is a
dated id such as claude-sonnet-4-5-20250514 for claude-sonnet-4-5). Only
that one diagnostic is adopted at init on purpose: the rest of the set
(missing skill refs, wiring, flows) is Meerkat’s to refuse at create time.
meta_gateway has no pre-check; Meerkat runs the same validation when it
creates the mob.
Provider-annotated profiles bypass the refusal. By Meerkat’s rule a
profile that sets provider = "..." is resolvable whatever its model string
says, so a dated or misspelled id on such a profile (every profile in the downstream app
carries provider = "openai") passes init and surfaces at the first LLM call.
rpc_gateway does not add a second refusal authority for that case; it logs
one WARN per such profile at init naming the profile, the model and the closest
catalog ids (profiles.<p> model '<m>' is provider-annotated ... a typo surfaces at the first LLM call. Did you mean one of: ...?). A provider = "self_hosted"
alias is checked against the host config’s [self_hosted.models] instead and
warned about only when it is not registered there. Catalogued and
[models.<id>]-defined models warn about nothing.
Earlier releases compared the model string against the static catalog alone,
so a [models.<id>] custom model and every self-hosted alias were refused at
init on rpc_gateway while meta_gateway accepted them and then failed at
the first member build with “self-hosted model ’..’ is not registered in
config”; both binaries now agree, and with meerkat_config_path the member
builds.
Provider credentials
An agent factory resolves a provider binding’s persisted credentials (API keys and the OAuth tokens a login saved) from a credential store, and writes refreshed tokens and logouts back to it. By default that store is Meerkat’s process default, the file store at<config dir>/meerkat/credentials, where
<config dir> is the OS user’s configuration directory ($XDG_CONFIG_HOME,
else ~/.config, on Linux; ~/Library/Application Support on macOS). A
credential lives at <root>/<realm>/<binding>.json, and refresh locks under
<root>/refresh-locks.
Everything that shares a store shares provider logins: every runtime in one
process, and every process of one OS user, unless they name their own. Two
runtimes whose agents use the same realm and binding ids read and write the
same credential file, so one user’s login replaces the other’s and one
logout removes it for both. A single-user host needs nothing more. A host
serving several users names a store per user:
The root is the credentials directory itself, an absolute path. The gateway
opens it once at init, creating it when missing, and every agent factory of
the launch holds that one store, with one refresh authority: the members and
the agents they create (helpers, forks, child mobs and councils), live
sessions, the agent memory engines, and the host auth and realm MCP
administration a native host composes. An empty or relative root is refused
with
-32602, and a root the gateway cannot open refuses init with
-32014: a launch that names a root never falls back to the process default.
Launches that name the same root share its credentials, and its file lock
serializes their token refreshes. meta_gateway adds the root to its resume
key, so a launch never resumes a runtime that keeps its credentials
elsewhere.
Pass the same root on every reopen of a user’s state. Nothing records which
root a state directory was launched with: a launch that names another root,
or none, runs that user’s agents on that other store, where their logins are
missing.
A host that kept its users apart with a per-process XDG_CONFIG_HOME=<C>
passes <C>/meerkat/credentials to keep their existing logins.
XDG_CONFIG_HOME moves the default store on Linux only: on macOS the
default is always under ~/Library/Application Support.
A host deploy cannot carry a named store: meta/mobpacks/deploy with
execute: true runs rkat mob run in a process of its own, which resolves
provider credentials in the process default store. A runtime whose agents
hold a named store therefore refuses such a deploy, on its RPC and on its
Flow Editor, with -32004 and data.kind = "provider_credentials_not_carried", whatever grant or opt-in the caller has.
A launch whose members build from a native realm or with a member auth
binding refuses it the same way, since that process would carry neither.
Deploy plans still answer.
GitHub Copilot with a service-account token
From Meerkat 0.9.9, Meerkat’s Copilot routes can authenticate with a GitHub token the operator supplies instead of a device sign-in (auth_method = "github_copilot_token"; see “GitHub Copilot with a service-account token” in
the Meerkat
auth guide).
Meta adds nothing to it. The routes are [realm] tables, so they go in the
host config (meerkat_config_path, or a native realm’s documents), and every
member resolves them like any other binding. With the guide’s
source = { kind = "env", ... }, the token comes from the gateway process’s
environment: COPILOT_GITHUB_TOKEN, then GH_TOKEN, then GITHUB_TOKEN.
With source = { kind = "managed_store" }, it comes from the credential store
above, where rkat auth login copilot --non-interactive stores it when the
gateway uses the process default store. Name the route for every member with
member_auth_binding, for example
{"realm": "global", "binding": "copilot_anthropic"}.
Native realm and member auth binding
A host whose provider setup lives in a native realm (a realm a native Meerkat host keeps, with its bindings and their credentials) launches the SDK gateway’s members from that realm’s whole configuration, with the binding it selected:runtime_options; sent at the top level of the
meta/init params, either is refused with -32602 naming its home.
The members’ configuration. At init the gateway resolves the realm with
Meerkat’s StorageLayout, checks with Meerkat’s own realm probe that the
realm exists there, maps the realm and its chain to their documents with
FilesystemRealmConfigSource (<state_root>/<realm_id>/config.toml, its
parents’, and the global document <user_config_root>/.rkat/config.toml),
composes them with EffectiveConfigReader, applies Meerkat’s environment
overrides and validates the result with Config::validate, as a native host
does. The explicit state_root wins over every probe, so context_root
selects no document: it is passed as Meerkat’s invocation context. Nothing
is copied, merged with another source, or written, and no path is derived
beyond what Meerkat derives from the four inputs. The reserved global
realm keeps no manifest; naming it reads the user document.
That configuration is the members’ whole configuration, not only its
[realm], [self_hosted] and [models] tables: [comms], hooks, skills
repositories, [model_fallback], [limits], [agent], [confinement] and
[governance] apply to every member as well. Only the gateway’s own
overlays (member_comms_address, compaction) are layered on top, as for
meerkat_config_path; a realm document that pins comms to a fixed address,
for example, sends every member there unless member_comms_address
overrides it.
The members’ binding. member_auth_binding is Meerkat’s typed
AuthBindingRef. Meerkat’s own explicit resolution checks it at init: its
realm must be in the members’ configuration and the binding must be defined
on that realm’s chain, where it resolves at the realm that owns it. Every
profile of the definition must then resolve to the provider the binding’s
backend serves: an inline profile, and a realm-referenced one
(realm_profile = "..."), which init resolves from the mob storage’s realm
profiles, where Meerkat resolves it at each build (one the store does not
hold is left to that build, which refuses it). The provider is the profile’s
own provider, else the one the members’ model registry (with the
definition’s [models.<id>]) owns the model under. A model registered under
another provider is refused, and so, under a self-hosted binding, is a model
the configuration does not register with a server or one served by a server
other than the one the binding’s backend declares.
Every member the gateway builds then uses exactly this binding, or its build
is refused: the members of the identity roster, helpers and forks
(meta/spawn_helper, meta/fork_helper), later identity-first spawns,
meta/spawn_member and meta/attach_existing_session members, members
agents create with their mob tools, respawns and reset successors, members
rebuilt on their own sessions after a restart (the launch’s binding takes the
place of the one the session recorded), and the members of the child mobs
agents create. A spawn that names the same binding is accepted; one that
names another binding (another realm, binding or auth profile) is refused
with Meerkat’s wiring error, even for a member placed on another member
host, which is otherwise built there with that host’s configuration.
Wherever the gateway compares a binding with the launch’s (a spawn, a
fallback target, a declaration, an attach, a member build), the launch’s
binding counts as named or stamped with the realm that owns it, the form
Meerkat records it in (a binding defined on a parent realm or on global is
recorded under that realm).
Meerkat rebuilds some members from their own records without asking the
gateway: a durable fork (an agent’s fork_off child, a live delegation
fork, a council participant) repeats its source’s build from the records
its source’s session keeps, and keeps doing so on every later rebuild. Such
a member builds with the binding its session recorded. A durable fork this
launch seats records the binding its source’s session records, which is the
launch’s once the source has been built under the launch (a member that
forks itself has been); one seated before the launch named the binding,
which records no binding or another one, is refused at its rebuild
(member_auth_binding_uncarried) and reads broken: re-create it under the
launch. A meta/fork_helper fork is not one of these: like a
meta/spawn_helper helper, it is a new member the gateway builds with the
launch’s binding, which only reads its source’s history, whether or not the
launch has woken the source.
The gateway refuses every path that would make a provider call off the
binding, rather than let it happen:
- a definition whose
[spawn_policy]creates members automatically: Meerkat builds those members without the gateway’s spawn customizers; - an enabled model fallback chain with a target that names no
auth_binding(Meerkat would select a default binding for it) or another binding, in the members’ configuration, the definition’s[runtime.model_fallback], an inline profile’smodel_fallbackor a realm-referenced profile’s. Name the launch’s binding on every target, or disable the fallback; - image generation (
tools.image_generation = truein an inline or a realm-referenced profile): Meerkat builds an image generation executor for each image provider other than the member’s own by default binding selection. A spawn whose profile override enables it, or that explicitly enables image generation, is refused too; - the agent memory engines (
agent_memory.distiller,agent_memory.stewardenabled): they build their model clients from the default configuration with no binding; - the same in a child mob an agent creates:
mob_createwith an automatic spawn policy, a fallback target off the binding or image generation, in an inline profile or a realm profile one references, and atoolingprofile, inline or a realm profile, that would make such calls; an agent’s realm profile write (mob_profile_create,mob_profile_update) of such a profile is refused too; - identity declarations an earlier launch adopted, which the launch’s mob storage holds: Meerkat materializes a declared identity from its sealed declaration without the gateway’s spawn customizers, so init refuses declarations sealed with no binding or another binding, naming them;
- a durable child mob an agent created before the launch named the binding: a restore rebuilds it with the definition it was created with, so init reads every stored child definition and refuses one with an automatic spawn policy, a fallback target off the binding or image generation, naming it. A child whose destroy is pending is skipped (Meerkat only finishes destroying it); a child Meerkat keeps from running for another reason (its owning session archived or not observable) is still refused, fail closed, and one launch without the binding settles it;
- every member build that would still run off the binding or make such
calls, checked where the gateway’s session services build a member, and
again after an SDK session builder’s
callback/build_agentanswer is applied (aresume_session_idanswer seats the build on another stored session; the resume itself is not refused). Refused are: a member build on another binding or none (none is Meerkat’s default binding selection: a fork seated before the launch, above); Meerkat’s fallback web search, enabled by the build or, on a member rebuilt on its session, by the tooling that session recorded, where the provider’s own web search does not serve it (a model without native search, judged as Meerkat’s factory judges it on the model the build runs on and the[models]of the member’s own mob definition; a copilot backend; or tool governance that disables the provider’s native tools: a dispatch admission, an application tool policy, a restricted tool access policy, or a host’s member registration hook, for whose decision Meerkat prepares every member the hook registers, governed whatever its application tool policy); image generation; and a fallback target off the binding. The provider’s own web search runs on the member’s binding and is not refused. The build is refused, and a member Meerkat builds that way readsbrokenwith the reason, at ready or at its first wake (below); mob/adopt_member_identity_declarationandmob/apply_member_tool_declaration, on the stdio gateway and onPOST /console/rpc: Meerkat materializes a declared identity from its sealed declaration without the gateway’s spawn customizers, so a launch withmember_auth_bindingrefuses both (-32602,member_auth_binding_uncarried) before anything is written;meta/attach_existing_session, ormeta/ensure_memberwith aresume_session_id, of a session that already has a live actor in the gateway’s process, on the stdio gateway and onPOST /console/rpc: Meerkat reuses that actor as it is, with the binding it runs, so unless the session records the launch’s binding, in either form, the attach is refused (-32602,member_auth_binding_conflict, withsession_id) before the spawn. A session whose binding cannot be read is refused too, withretryable: trueindata: the same call can succeed once the read does. A session without a live actor is rebuilt with the launch’s binding.
broken with the
reason, and the wake answers the refusal. The roster pass, the reset and
the delete themselves answer as they do without a binding. The exception
is an identity that owes a kickoff (runtime_mode autonomous_host, or
an initial_message): Meerkat spawns it live at the roster pass, or at
the reset when it does not hold the identity, so the build check judges
that spawn there. Its refusal is final too: the seat answers it as the
identity’s refusal and is not retried on its own, and a refused reset
keeps the identity’s current state and leaves nothing to carry out after
a restart. An archived session’s revival and a declared role migration,
which the roster pass carries out on the identity’s own session, are
builds as well, judged and refused the same way.
Under the default member_restore ("on_demand") a seated member is built
at its first wake, not at start, so a refusal at its build (the build check
above, judged against that build’s own model registry, or a durable fork
recorded off the binding) comes at that wake, which answers it for good;
until then the member reads active like any seated member.
"on_demand_verified" checks only that each session can be resumed, so it
does not catch these refusals. A launch that changes its binding and wants
them at ready restores with member_restore: "eager".
An agent meets these refusals typed: its mob tool answers a policy denial
whose code is member_auth_binding_conflict (mob_spawn_member naming
another auth_binding) or member_auth_binding_uncarried (mob_create, a
tooling profile of mob_spawn_member or delegate, inline or a realm
profile, or a mob_profile_create or mob_profile_update, that would make
provider calls off the binding), before the tool runs.
A candidate launch that boots the stored definition
(mob_composition.candidate_definition: "stored") is checked against the
definition it boots as well.
Credential scope. The options name the route, not the credentials’
store. With provider_credentials_root, the binding’s credentials are read
and refreshed in that directory, with nothing copied: <root>/<realm>/<binding>.json,
<root>/<realm>/<binding>@<profile>.json for a binding’s auth profile, or
<root>/<realm>/account~<account>.json for a binding with a credential
account, under one refresh lock. <realm> is the realm that owns the
binding, which can be a parent of the realm the option names. Without
provider_credentials_root, the process default store of the gateway’s user
applies: launch the gateway with the native owner’s HOME (and, on Linux,
XDG_CONFIG_HOME) to share the native owner’s store.
Generations. The options are read once, at init. A running gateway is
never rebound: a changed realm document or binding takes effect in the next
launch, which rebuilds each member with its own binding when it builds it
(at start under member_restore: "eager", otherwise at the member’s first
wake), except the durable forks Meerkat rebuilds from their own records
(above). Pass the same options on every launch of the same state. A member
whose stored profile override carries a fallback target bound to an earlier
launch’s binding, and a durable fork seated under an earlier launch’s
binding, are refused at their rebuild or respawn under a new binding and
come up broken: re-create such a member.
Refusals. Each answers meta/init (or its failed settlement) before any
agent factory is built, with error.data.kind. A realm the state root cannot
resolve, and launch state init cannot read to check it against the binding,
are storage refusals (-32014); a realm document that does not read,
compose or validate is invalid params (-32602), as a meerkat_config_path
file is.
A launch with either option refuses an executing
meta/mobpacks/deploy
with provider_credentials_not_carried (see
Provider credentials). Live channels keep their own
binding options (openai_live, console_voice).
A member build that the gateway refuses (a fork seated before the launch, or
a member rebuilt on a session that recorded the fallback web search, say) is
not an init refusal: it fails the build with Meerkat’s
SessionError::FailedWithData, whose message is
member_auth_binding_uncarried: ... with the reason and whose data carries
kind and reason. A caller meets it as the error of the method that built
the member: the error message contains member_auth_binding_uncarried: and
the reason (behind Meerkat’s session error: prefix), and
meta/spawn_member, which answers -32602 (Invalid params: ...), also
carries error.data = {"kind": "member_auth_binding_uncarried", "reason": ...} when the refusal reaches it as that typed error. A member Meerkat
builds reads broken with it, at ready or at its first wake (above).
Known limitations. These are refused, never run off the binding:
- Meerkat’s auxiliary tool executors (image generation, the fallback web
search) resolve their credentials by default selection, never with the
member’s binding, so whatever would enable them under a member auth
binding is refused (
member_auth_binding_uncarried): image generation at init, at the spawn and the agent tool, and at the member’s build; the fallback web search at the member’s build, where the provider’s own web search, which runs on the member’s binding, passes. - Meerkat rebuilds a durable fork from its own records, with the binding its
session recorded, so a durable fork seated before a launch named its
member auth binding is refused at its rebuild under that launch
(
member_auth_binding_uncarried) and has to be re-created. A new durable fork copies its source’s records the same way: after a launch changes the binding, a source this launch has not built yet (a seated member nothing has woken, under the defaultmember_restore) still records the earlier binding, and a durable fork of it is refused. Wake the source first (a delivery, ormeta/identity/warmfor an identity), or restore withmember_restore: "eager". Ameta/fork_helperfork is built with the launch’s binding either way. - The agent memory engines build their model clients from the default
configuration, so a launch with a member auth binding refuses them at init
(
member_auth_binding_uncarried). A later release can build them from the members’ configuration and binding.
Member role migrations
role_migrations is a top-level meta/init param, a sibling of
runtime_options rather than a field inside it. On rpc_gateway,
runtime_options is a closed allowlist and unknown keys are a hard error, so
nesting the declaration there fails initialization with
unsupported runtime_options fields: role_migrations.
A durable member whose role changed refuses to resume: Meerkat returns
MobError::MemberRoleMigrationRequired rather than silently restamping the
member’s durable role, comms name and binding together. The host clears that
refusal by declaring, for one boot, which exact identity is migrating and which
role it is migrating from.
.role_migrations([...]) accepts RoleMigrationDeclaration dataclasses or
plain dicts and validates both; a conflicting pair raises ValueError; leaving
the setter unused emits no role_migrations key at all. The TypeScript builder
exposes no equivalent setter.
The two gateway binaries, whose names differ by one word, refuse bad payloads
differently and install accepted ones under different conditions.
Declarations that reach neither identity plane arm nothing, because there is no
identity plane to migrate on. See
roster and member lifecycle for the operator
view of the refusal.
Fork instruction policy
runtime_options.fork_instruction_policy decides what happens to a member’s
activated instructions when that member is forked. This section explains what
a fork copies, what each policy does, what reactivate_inherited writes and
trusts, and how to set the policy.
Activated instructions
A host changes a member’s standing instructions withmeta/member_activate_instruction: each new activation appends one
keyed, immutable activation row to the member’s session, for example the body
of a skill your application keeps under namespace app.skills and key
calendar. For each namespace and key, the newest row that the member’s own
session wrote is the instruction in effect. Meerkat treats every namespace
alike, so a skill, a persona, a standing rule and any other key are all
activation rows, and the policy applies to every one of them.
Instructions a member gets from its build (its profile, the skills its profile
preloads included, and its system prompt) are not activation rows. The policy
does not concern them.
What a fork copies
A durable fork copies the source member’s committed transcript, up to the fork boundary, into a new session and seats that session as a new member. The durable forks are the agentfork_off tool, UnifiedRuntime::fork_member, a
live delegation’s fork of the member, and the participants of a temporary
council. (meta/fork_helper is not a durable fork, and this policy does not
apply to it. It renders the source’s transcript as text for a fresh member. A
helper that runs as an autonomous host gets that text as its first prompt, and
with the default full-history context it includes the source’s system rows, so
the bodies of the source’s activated instructions reach that helper as plain
context under every policy, refuse included.)
fork_off also gives the child the source’s role profile, its profile and
model overrides, and the forking member’s own tool access policy. The forking
member needs spawn admission for that role: manage scope over the mob, or
spawn scope for the profile.
Inherited activation rows stay in the child’s transcript with their full text,
rendered for the child as [meerkat-inherited-instruction-v1] with
status=historical_only_not_active_in_child. The child can read what its
source was told, but none of it is in effect. A child runs no model turn while
an inherited key has no activation of its own: its first model request fails
with forked session must explicitly activate inherited instruction keys,
followed by the keys. The policy decides who writes that activation, or
refuses the fork instead.
The three policies
The Console’s side chat fork (
meta/fork_member) is a host-driven fork whose
host is the Console. Under require_explicit_activation it activates in the
side chat, as the host, each inherited key whose head the source activated
itself, with that head’s revision and body, superseding the copied row; any
other inherited key stays inactive, the side chat refuses its first turn
naming it, and the fork’s result lists it (see
meta/fork_member).
A source with no activation rows in the copied prefix forks the same way
under every policy. The default is the least-privilege choice: no durable
fork’s child runs an instruction that nobody activated for that child.
Under require_explicit_activation, a Rust host activates an inherited key on
the child with MobHandle::activate_member_instruction(child, request)
(runtime.mob_handle()). Read the key first with
MobHandle::read_member_instruction_activations and a query that names its
namespace and key: for an inherited key, key_state reports
requires_explicit_child_activation: true, next_expectation
{"kind": "absent"} and next_supersedes set to the inherited row’s
activation_id. The host may activate the same revision and body, or a
different one.
What reactivate_inherited writes
Before the child is committed, the fork appends one activation row per
inherited key, in namespace and key order:
- Keys: every key with an activation row in the copied prefix, in every namespace. There is no namespace filter.
- Revision and body: the key’s head at the fork boundary, which is its
last copied row. The new row carries that row’s
revision(namespace,key,revision_id,content_sha256) and its whole body, unchanged. An activation the source wrote after the fork boundary is not in the prefix and is not copied. - Origin: the child’s own session. The key is then in effect for the child
exactly as if the host had activated it there. The row
supersedesthe copied head’sactivation_id. - Identity:
activation_idisfork-reactivation:followed by the first 32 hex digits of SHA-256 over the child session id, a zero byte and the head’s activation id. It depends only on the child and the head, so the same fork writes the same rows. The prefix is for operators; Meerkat recognizes these rows by reproducing them, not by their ids. - Timestamp: the last copied message’s timestamp.
- Rules: the rows pass the ordinary activation rules: the body digest, 256
KiB per body, and the 1 MiB budget of live activation bodies in the
session’s transcript, copied rows included. Each re-activation adds its body
again until the child’s first compaction retires the copied rows it
supersedes, so a chain of forks of a member with large bodies that does not
compact between forks can reach that budget and is refused with
lineage_body_budget_exceeded. - Only what the source activated itself: each head must be an activation the
source session wrote itself. A head the source only inherited from an
earlier fork, and never activated, refuses the fork with
not_effective_in_source. - Only a body the source still holds: a compaction retires the body of
every activation row that a later row of its key superseded. A fork whose
prefix ends between such a row and the row that superseded it has no body
to carry and is refused with
head_body_retired. A fork at the tail copies, with its body, each key’s newest row the source activated itself: a compaction never retires it.
What reactivate_inherited trusts
With reactivate_inherited, the child claims its source’s activations as its
own:
- The copied body is the one in effect in the source at the fork boundary. Meerkat does not ask your application whether that revision is still current, still allowed, or meant for this child. A skill revision you have since withdrawn, replaced or narrowed keeps running in a child forked from a transcript that still carried it.
- Once written, the row is the child’s instruction for that key, like one your host activated on the child. An activation is never removed: to change what the child runs, activate a new revision of the key on the child, which supersedes the re-activation row.
- A fork of the child copies those rows again, so one setting carries an instruction down a chain of forks.
- The policy is one decision for every member, every key and every fork of the
gateway’s mob. Choose
reactivate_inheritedonly when every instruction your members may carry is safe to run, unchanged, in any fork of them.require_explicit_activationkeeps that decision with the host for each child (and refuses forks that leave it no chance), andrefuserules out durable forks of activated members (an autonomous-hostmeta/fork_helperstill sees their text).
Refusals
A refused fork commits nothing: no child session, no member and no transcript. Meerkat classifies the refusal asFORK_INSTRUCTION_REFUSED
(-32037 on Meerkat’s own JSON-RPC, HTTP 422, CLI exit 57). The Console’s
side chat fork (meta/fork_member) answers it as -32037; no other method of
this gateway forks a member, so elsewhere you meet the refusal as the agent’s
fork_off tool error, a council exit reason, or
MobError::ForkInstructionRefused { source_member_id, refusal } from the Rust
API. Its structured data:
The message names the keys, for example:
fork_off gets a tool error with that message after
tool 'fork_off' failed: and the structured data above. A temporary council
ends with the exit reason participant_fork_instruction_refused, naming the
participant’s order and the refusal; nothing was created for that
participant, so its cleanup has nothing to revoke.
Setting the policy
The policy belongs to the host. It is never a tool argument or part of a mob definition, and it cannot be chosen per fork: every fork of the gateway’s mob uses it. The gateway applies it to its mob on every create and resume. It is not stored with the mob, so set it on every boot. A fork applies the policy the gateway runs at the moment of the fork; changing the policy later does not touch children that already exist.
Meerkat’s own JSON-RPC server forks sessions with
session/fork_at and
session/fork_replace; those calls take the policy per call as
inherited_instructions. They are not this gateway’s member forks.
Example: a member with an activated skill
The host activates a skill on memberassistant:
assistant calling fork_off now gets the
fork_instruction_refused tool error above, and nothing is forked. A host
that wants forks to keep the skill opts in at boot:
fork_off child then starts with its own activation of
app.skills/calendar, with the body assistant had at the fork boundary, and
its first turn runs.
Child mob tool policy and bundles
A child mob is a mob a member creates itself: the agentmob_create tool, and
the implicit mob delegate helpers run in. Mobs the host creates, same-mob
fork_off and mob_spawn_member, and temporary councils are not child mobs;
their members keep their own bindings.
child_application_tool_policy
Once the host serves application tool policies (the application_tool_policies
init parameter, or with_tool_consequence_policy_registry in Rust), every
child mob member needs an application tool policy the host chose. Without one
a governed member could create a child mob whose members run outside the
host’s policy, so Meerkat refuses mob_create and delegate until one is
configured. There is no implicit default.
The refusal is a typed tool error, not a run failure: the member’s turn goes
on and the model sees policy_denied with code child_tool_policy_required
and a message that says why (the host runs a tool-policy registry and no child
application tool policy is configured) and names the three fixes below.
The value is an ApplicationToolPolicyBinding object, a top-level
meta/init parameter next to application_tool_policies:
{"kind": "inherit"} is refused: a child mob has no member to inherit from
when it is created. The gateway checks the value at init and refuses the boot
with -32602 when it is malformed, inherit, or a provider binding whose
provider or policy no application_tool_policies entry carries, so a binding
that could never resolve does not surface later as a refused delegate.
- Python:
Meta.builder().child_application_tool_policy({"kind": "provider", "provider_id": "example", "policy_id": "example-tools"}). - Rust:
MobBootstrapSpec::with_child_application_tool_policy(binding)orUnifiedRuntimeBuilder::child_application_tool_policy(binding). A conflicting binding from the two refuses the build withConflictingConfiguration.
Child-available tool bundles
Host Rust tool bundles are host-only by default: agent-created child mobs do not receive them. Register a bundle as child-available to supply it to child members:UnifiedRuntimeBuilder::register_tool_bundle_with_availability takes the same
arguments. When a child mob is created, every child-available bundle id is
supplied to each inline profile of its definition and persisted with it;
callers never name bundles, and naming rust_bundles in an agent’s profile
input is refused. The child profile’s deny list still narrows the supplied
bundle tools, and the child application tool policy governs them like any
other tool. A bundle the host later stops registering is neither dropped nor
granted: resuming a child member that was built with it refuses with
MobError::ToolBundleUnavailable { bundle }.
Mob profile tools ([profiles.*.tools])
The mob definition (.mob("config/mob.toml"), or the mob_config string of
meta/init) is Meerkat’s MobDefinition. Each
[profiles.<name>.tools] table is meerkat-mob’s ToolConfig, and every
boolean in it defaults to false. Raw ToolConfig serde deserialization
ignores unknown keys, but MobDefinition::from_toml checks profile/tool keys
and warns about ordinary ignored unknowns; MobDefinition::parse_toml exposes
typed diagnostics. A typo such as comm = true still does not enable comms.
These TOML diagnostics do not make all unknown keys fatal and do not apply to
bare JSON/serde deserialization.
Dropping a key the parse warns about and ignores is safe on a persistent store.
The key never reaches the typed definition, so it is not part of the
composition Meta records when it creates the store or compares on resume.
Removing such a key (for example mob_tasks under [profiles.<name>.tools],
which configs written for earlier releases carry), or adding one, never makes
the store refuse as divergent; a divergence refusal names only keys the
definition accepts.
The
mob-communication skill is preloaded for every member regardless of these
flags. Agent-side spawns that inherit the parent’s tool filter open every
category, including comms, before the inherited allow-list is applied, so the
comms refusal is a host-spawn (meta/ensure_member) behaviour; an agent
spawn of a comms = false profile silently runs with comms on.
WorkGraph
rpc_gateway constructs a realm-scoped WorkGraphService on every launch
unless runtime_options.workgraph is false (the default is true, so a
launch that never mentions the key has one). The service backs the member
workgraph_* tools, the mob-executor attention overlays and the
meta/workgraph/* RPCs.
meta_gateway has no switch: its persistent mode attaches the durable store
from the workspace state dir (same degraded posture on open failure) and its
default ephemeral launch is memory-backed.
The namespace grant is issued by the gateway, not the host. Meerkat
refuses to build an agent that has WorkGraph tools and no host-issued
WorkGraphNamespaceGrant. Whenever Meta installs the tools it takes the
grant from the service it just built, so the grant names the same scope the
tools are pinned to: realm mob.<mob definition id> (Meerkat’s canonical mob
realm, the one every other realm-scoped store for the mob uses; if it cannot
be formed the raw id is used and Meerkat’s own validation names both realms)
and namespace default. Every workgraph_* tool call has its realm_id and
namespace arguments overwritten with that scope and all_namespaces
removed, so a member cannot wander into another mob’s graph. The meerkat
WorkGraph guide’s “embedding hosts must issue the grant in the agent build” is
a Rust-embedder instruction; SDK hosts have nothing to issue. The tools stay
per-profile: a member sees them only when its profile sets
tools.workgraph = true, while the meta/workgraph/* RPCs are available
whenever the service exists.
Mob announcements
Mob announcements are composed the same way on every surface that offers them. The options are checked once, before anything is built; themob_announcements member tool bundle is registered for the host’s own
profiles, before the runtime is built, and offered to child mob profiles
only under member_tools_in_child_mobs; and the owner attaches once the
identity runtime is installed, before the control listener’s keyed inject
plane is sealed. Each surface refuses what it cannot compose.
member_tools_in_child_mobs: true sets the same ceiling on every surface: a
child mob profile gets the member tools only by opting in itself, with
tools.bundles = ["mob_announcements"].
An SDK host sets runtime_options.mob_announcements with the builder:
Meta.builder().mob_announcements({"enabled": True, ...}) in Python and
Meta.builder().mobAnnouncements({ enabled: true, ... }) in TypeScript send
the object unchanged, and mob_announcements() or mobAnnouncements()
alone sends {"enabled": true}.
Mob announcement source runners
A source may name the collector that advances it:"runner": {"host_runnable": "<name>", "every_seconds": <n>}.
The host owns the runnable’s code: it registers the name in
runtime_options.host_runnables and handles each fire (host_runnables
and on_schedule_fire in the Python SDK). Meta schedules the runnable for
every live source that names it, through the gateway’s schedule service,
first at the declaration and then every every_seconds (60 to 2,592,000).
A changed cadence restarts the interval at the change. Each schedule Meta
makes for a runner is labelled mob_announcements.source_runner, and Meta
never changes or ends a schedule without that label. sources/put refuses a
runner that names a blank runnable or the memory steward’s (-32602).
A runner on a gateway that schedules none, such as the console gateway or
one without a schedule service, never fires there: such a source collects
only when the host advances it itself.
A peer gateway refuses this gateway’s keyed injects issued further from its
clock than the window the owner assumes, which bounds when a batch for a
member on that peer may expire. rpc_gateway takes it from
runtime_options.control_request_window_ms. meta_gateway’s control
listener serves keyed injects with the default window,
DEFAULT_KEYED_INJECT_WINDOW_MS (five minutes), and its owner assumes the
same. A runtime the builder composes takes the window its keyed inject plane
serves with (UnifiedRuntimeBuilder::keyed_inject_plane, by default
DEFAULT_KEYED_INJECT_WINDOW_MS) in place of the configuration’s
peer_request_window_ms, which applies only when the plane does not serve.
The runtime’s shutdown stops the delivery dispatcher, whichever surface
composed it.
Mob announcement declarations
runtime_options.mob_announcements may declare sources and subscriptions:
inline, as "declarations": {"revision": <n>, "sources": [...], "subscriptions": [...]}, or as "declarations_path": "<file>", a packaged
JSON file holding the same object, which the host supplies and Meta reads at
init and again at each declarations/apply; it is never a member input. A
gateway takes one of the two: naming both refuses the configuration, and so
does a sidecar that cannot be read or parsed. At startup Meta checks the
declarations against the gateway’s mob, and declarations the static checks
refuse fail init as a refused announcement configuration does; it then
reconciles them before its dispatcher starts, waiting at most 30 seconds.
The options’ declarations take effect when nothing is applied yet or they
name a higher revision than the applied one. meta_gateway takes the same
keys in its mob_announcements init param. The schema and the reconcile
rules are in Mob announcement
declarations.
Storage layout and durability
MetaStorageLayout is the single path authority for storage roots and the
canonical top-level database locators; no gateway or builder surface derives
state-dir file names or resolves ambient roots ($XDG_STATE_HOME, $HOME)
on its own, and a CI gate (tests/storage_gate.rs) keeps it that way.
Feature code owns relative names beneath the layout’s roots (the blob
directory’s internal sharding, per-realm agent-memory files).
Canonical spellings (decided once; stores shared with Meerkat keep
Meerkat’s names, Meta-owned files converge on *.sqlite3):
0.9.4 keeps every session in Meerkat Core’s store in
runtime.sqlite. It
creates and opens no session database, and keeps no session rows in the
continuity database. An unretired sessions database under any of its
spellings, or a continuity database under any of its spellings that still
holds session rows, is 0.9.3 state: startup refuses it
(NativeStorageMigrationRequired, -32014) until
meta_gateway storage-migrate-native migrates it in place (see
Upgrading to 0.9.4).
On normal persistent gateway launches, event_log.sqlite3 holds Meerkat’s
canonical mob definition, events, and adopted-member state. It is live storage,
not a reserved file, and is distinct from the optional operational query
projection configured by runtime_options.event_log. The SDK gateway’s
explicit runtime_options.mob_storage: { "storage": "memory" } declaration
keeps mob storage in memory, except under persistent_state while a roster
provider is registered: the roster’s identity bindings live in the mob
storage, so that launch is refused (-32014). The Rust builder refuses the
same composition (mob_storage() without a persistent path, together with
persistent_state() and roster_provider()) with
ConflictingConfiguration; without mob_storage(), persistent_state()
keeps mob storage in event_log.sqlite3.
Canonical-name-first probing. Opening a slot resolves the canonical name
first, then probes the known legacy spellings in the same directory (for the
slots 0.9.4 opens, the continuity database included, which keeps the identity
records; the sessions spellings are read only by the migration verbs and the
storage doctor). Exactly
one spelling → it is used where it lies (no rename at open). Two spellings
of the same store → the gateway refuses to start (error -32014, “file-name
twins”) and points at meta/storage/doctor; physical renames happen only
through the storage migrate verb, under the maintenance fence.
Durability classes and the declaration requirement. Every composed slot
resolves to a durability class (durable / scratch) and a resolution
(persistent / declared_ephemeral / non_persistent). A durable-class
slot may run non-persistent only as an explicit declaration
(runtime_options.event_log, the Rust builder’s ephemeral_blobs(true), and,
without persistent_state, runtime_options.runtime_store and the builder’s
ephemeral_runtime_store(true)), never as a silent fallback. Under
persistent_state an in-memory runtime store is refused (-32014, or
ConflictingConfiguration from the builder). Undeclared gaps are startup
errors that SDKs surface as the typed StorageResolutionError (-32014). The sanctioned boot-without
degradations (schedule or workgraph store open failure) disable the feature
and appear as degraded slots.
Storage census surfaces. The resolved picture rides the storage object
on meta/status, meta/capabilities, and meta/storage/doctor:
blob_durability / blob_store_persistent (H1), session_store_incremental
(H2), and the per-slot slots array (domain, class, resolution,
backend, degraded, optional detail).
Incremental session persistence. session_store_incremental: false means
the session store writes the whole session document at every turn boundary.
Rust embedders can opt in to strict mode with
UnifiedRuntime::builder().require_incremental_session_persistence(true).
build() then refuses with SessionStoreNotIncremental instead of starting
on that fallback.
Changelog policy. Any storage file rename or table move gets the
binary-rename treatment: an explicit, loud changelog entry naming the old
and new spellings — operators read these files directly.
Identity bootstrap
- Rust
- Python
identity_bootstrap_mode setter is an explicit
identity-first configuration and therefore requires a roster provider for all
three modes. Leave the setter unused to retain a classic non-identity gateway;
a configured roster with no mode reports the eager_materialize mode.
Under every mode, the roster pass makes each roster identity a member of the
mob without materializing it: an identity the mob already holds needs
nothing, and one it does not hold is seated, bound to a fresh session (or to
the session a first-boot row names), in batched commits. The mob materializes
a member when work reaches it, from the host or from a peer, and at start
only as member_restore says. Every seated member reads active from ready,
materialized or not.
A retire of an identity member (meta/retire, or meta/retire_member with
the member’s id) releases it: the member stays seated on its session at its
generation, its delivery scopes stay valid, and it wakes again on its
identity, with the same incarnation, when work next reaches it. Core
releases only a quiescent member: while the member is being woken or has
work in flight, the retire is refused, retryable (-32052), and nothing is
interrupted. An incarnation ends with a reset or a delete, which Core
carries out as one rotation onto a fresh session, with a respawn, which
wakes the member on its session at a new generation, or when Core retires
the member: for a roster that no longer lists the identity, for a
meta/compact_member rebuild, and for a meta/reload_member over a session
bridge without Core’s registration reload, the last two waking it again on
its session at a new generation.
A placed or externally bound identity has no seat: its host creates its
member. Under eager_materialize the pass creates it through its host, so a
host that is not bound yet makes it read broken at ready. Under the lazy
modes the pass leaves one the mob does not hold for its first use, as these
modes always did: it reads dormant (and the bootstrap reads not ready)
until its first delivery, or the background warm, creates it through its
host.
lazy_with_background_warm adds a warm after ready: a tracked background
task asks the mob to materialize every roster identity, at most the mode’s
concurrency at once, or META_IDENTITY_RESTORE_CONCURRENCY (16 by
default) when the mode sets none, and leaves the bootstrap snapshot of every
seated identity as the pass published it. A roster refresh stops the warm
without waiting for the materializations in flight, which the mob finishes
on its own; a shutdown waits for them.
meta/status_identity_bootstrap returns a non-blocking typed snapshot.
meta/wait_identity_bootstrap accepts target = "materialized" or
"startup_ready"; terminal broken identities return with ready = false
rather than leaving the barrier pending indefinitely. Provider or restore
failures that apply to the whole pass are reported in the snapshot’s optional
error field, and so is a read of Core’s member history or of its broken
members that failed during the pass: the snapshot is then not ready, since it
could not tell which identities Core holds. An explicit materialization or
meta/identity/warm that Core refuses with a refusal that clears on its own
(-32052, a pending first-boot row, an owed held completion, a full
admission backlog) leaves the identity’s entry as it was, unless Core holds
its member broken; any other refusal marks the entry broken.
A candidate launch (runtime_options.mob_composition.authority = "candidate") does not break an identity whose wake would change the stored
roster or its journal: an identity the store holds no member for, a member
its roster moves to another role, one whose stored session the runtime store
does not hold (a seat’s session its first wake has not created yet, or one
that is gone), a pending fresh first-boot row, or one Core holds a stored
session but no live member for (a dormant or retired identity, or a
first-boot Resume row), whose wake would spawn a member Core journals in
the shared mob store, or
one whose live member Meta 0.9.3 spawned without the durable-identity
marker, which the roster pass respawns onto its session. The
snapshot leaves such an identity out of identities and lists it in
deferred as {"identity": ..., "reason": ...}, with one reason, by
precedence first_boot_fresh, create_member, move_role,
fresh_session, resume_member, counted in counts.deferred. A deferred
identity counts neither as broken nor against ready, the pass completes,
and a candidate launch roster census WARN names the deferred identities.
The members Core restored live at start stay as they are. The
authoritative launch makes the change. Any other failure (a store error, a
lost comms identity, a refusal from Core or the host) still breaks the
identity. Both fields are absent on every other launch.
During the roster pass each identity’s entry also carries typed restore
progress: {"stage": "registered"}, "resuming" (a held pass is creating
the member), "minted" (the identity was seated, or a held pass created it,
on a fresh session), "resumed" (it is bound to its own committed session),
or
{"stage": "refused", "code": <code>, "kind": "<kind>"} with the refusal’s
error code (kind is absent when the refusal has none). Neither minted nor
resumed says the member was materialized. A seated member is active (and
a refused one broken or dormant, as its refusal says) as soon as its own
step settles, before the whole pass ends. In process,
IdentityRuntime::watch_identity_bootstrap_status streams the snapshot. On
the stdio gateway meta/init returns after the roster pass, which
materializes nothing, so it does not wait on the members’ restores.
No background pass retries a refused identity. A refused seat answers typed
and the identity’s next roster pass or delivery tries again; a member Core
cannot restore when work reaches it is refused to that delivery typed, and
reads broken once Core holds it Broken. The agent-memory member-event
observer keeps a last-resort fallback wake for a subscribe that fails or a
stream that closes as soon as it opens: 1 s, doubling to 30 s, reset by the
first stream that stays open.
The stock persistent gateway also advertises a private shutdown handshake and
its bounded stdio_shutdown_horizon_ms in the meta/init result. SDK hosts
keep stdin and provider callbacks open until that handshake completes, then
close the process. Honor the advertised stdio_shutdown_horizon_ms rather than
hard-coding it. The SDKs report the value they accepted as
runtime.gateway_shutdown_horizon_ms (Python) and
runtime.gatewayShutdownHorizonMs (TypeScript), None or null when the
gateway advertised no valid horizon, so a process that supervises the application can
wait for it. The current stock 432-second horizon covers an already-admitted
provider callback, runtime event and mob drains, the mob’s terminal shutdown
report, the durable child-mob stand-down, the member MCP settlement (in
the runtime shutdown and again at the gateway’s executor-exit boundary), and
bounded response-delivery/reaping margin. At the executor-exit boundary the
gateway answers only after every member MCP server’s close returned its
native outcome. That wait outlasts its share of the horizon only if Meerkat
breaks its own close bound, and a host that enforces the horizon may then end
the process before the answer. Provider operations
must resolve or reject within their public 120-second contract. The Python host
allows 125 seconds for event-loop completion before cancelling that callback;
the TypeScript host exposes the same cancellation as an optional
ProviderCallbackContext and suppresses late callback responses; the gateway
enforces a final 130-second hard wire deadline. Provider rejection,
timeout, or cancellation is pre-commit: an implementation must not commit a
replacement grant or other external authority after cancellation is observed.
Older and custom gateways that do not advertise the handshake retain the EOF
shutdown protocol.
If the gateway exhausts its runtime deadline, it reports incomplete cleanup;
the SDK still reaps the bounded child process and then surfaces the failure.
Letting in-flight turns finish on shutdown
By default a graceful shutdown does not wait for the turns members are running. The mob’s stop leaves each member’s current run running, and the mob’s terminal shutdown then settles it (meerkat 0.9.5):- A durable member, one whose session lives on a persistent session service and so commits each round, keeps its run: the run is suspended, not cancelled, and continues from its last committed round at the member’s next start. Its eligible queued prompts are kept for that start too, for up to 15 minutes.
- Any other member’s run is cancelled, and its queued inputs end with it.
meta/init parameter shutdown_in_flight_drain_ms
(Python MetaBuilder.shutdown_in_flight_drain_ms(ms), TypeScript
MetaBuilder.shutdownInFlightDrainMs(ms)). It is an integer of milliseconds.
Absent or 0 drains nothing, and the most is 300000 (5 minutes); init refuses
any other value.
The drain runs only for the SDK’s shutdown handshake and for SIGTERM, the
stop signal supervisors send. SIGINT, a closed stdin and a closed host shut
down at once, as before. With a window, once the dispatch loop has ended and
before the rest of the shutdown, the gateway:
- asks a host composition to stop admitting new work (see below);
- stops the schedule host from claiming occurrences: an occurrence that
falls due during the window stays due, and the restarted gateway handles
it under its misfire policy. A
skipoccurrence overdue past its grace is skipped, and a long window plus a slow restart can push acatch_up_withinoccurrence past its bound; - stops the HTTP listener from accepting connections (requests and streams already open continue);
- closes cross-mob control, identity foreground operations and runtime delivery;
- stops the mob with every member’s current run left running, and waits until each of those runs ends or the window passes.
stdio_shutdown_horizon_ms grows by the window plus 10
seconds for the drain’s own stop: 432000 + 10000 + the window. The SDKs wait
that much longer when they close. A supervisor that stops the gateway with
SIGTERM must allow at least that horizon, plus the time to reap the process,
before it kills it. Under Meta Forge, an application’s stop budgets follow the
horizon it reports at readiness. A stop before its first readiness report, or
before the first report after the window grows, uses the default horizon; a
turn cut off then recovers at the next start.
What a drained turn can still do:
- Mob-mutating tool calls (spawn, delegate, fork, wiring) are refused, typed, and the model sees the error.
- A message to a peer is held, and ends
Abandoned(Stopped)at the shutdown. A turn that waits on another member’s new run cannot finish, so it is still running when the window ends. mob_announcecommits, and subscribers receive the announcement after the restart.- WorkGraph writes commit, and their wakes stay pending.
- Identity foreground operations are refused, typed. A caller already
waiting on a turn (
meta/wait_for_turn) keeps waiting and gets the turn’s outcome. - A schedule the turn creates is stored, and fires after the restart.
- Detached jobs and monitors keep running, with their notifications pending in the inbox.
- Host callbacks are answered, under the handshake and under SIGTERM. An SDK host hit by the same signal may be stopping itself, so SDK-hosted gateways should stop through the handshake.
live_channel_open and the shutdown handles it as before. An autonomous
member whose current run is its long-lived kickoff loop holds the drain for
the whole window, and the shutdown then settles it as above. A held member
add or a roster pass still settling when the window ends cuts the drain
before its stop (identity_foreground_busy), and the shutdown settles it
inside its own budget.
The meta/shutdown answer carries the drain’s report as
result.in_flight_drain, and the gateway also logs it at INFO
(in-flight drain finished), which is where a SIGTERM stop reports it.
Without a window the answer has no such field.
Every report carries
window_ms; every report but skipped also carries
waited_ms.
A host composition (run_persistent_with_host) that admits work of its own
(source submissions, member adds, recovery steps) implements
GatewayHostComposition::stop_admitting, which the gateway calls once, before
the drain. The host keeps serving work already entered, and keeps the
credentials and custody in-flight turns use: begin_close still follows the
drain. An error skips the drain (host_admission_open). A host on the default,
which keeps admitting, can still submit work during the window: the mob’s stop
holds it, and it ends Abandoned(Stopped) at the shutdown.
Rust hosts that dispatch JSON-RPC from an Arc<UnifiedRuntime> should call
handle_unified_rpc_json_arc (or the corresponding with_live_arc entrypoint).
That transfers runtime ownership to supervised identity mutations, so a
disconnected request cannot cancel a cross-mob operation between its commit
and rollback points. The borrowed dispatcher fails identity-owned cross-mob
mutations closed because it cannot provide that ownership guarantee.
meta_gateway identity roster
identity_roster is a top-level meta/init param of meta_gateway
(typed InitParams), the standalone counterpart of the SDK roster provider
that rpc_gateway reaches through .roster(...) callbacks; rpc_gateway has
no identity_roster key. It is an array of DurableAgentSpec objects and seeds the
desired identity roster for identity_first: true (the default):
At boot the seed becomes the
MutableRosterProvider and restore_flow runs
over it: the roster is validated, Core’s member history is read for every
listed identity, and each identity is created or resumed. Omitting the
param is an empty seed, so the gateway starts with no identities and
meta/ensure_member adds them at runtime (it upserts the same provider). The
seed is boot-scoped like role_migrations: it is never persisted, and an
identity missing from the next boot’s seed is not restored on that boot (Core
keeps its session). Under identity_first: false the param is
parsed and ignored, not refused.
ConsoleUiConfig
View-level configuration for the stock console.meta_gateway discovers config/console.toml in the conventional workspace layout; rpc_gateway accepts the same file via runtime_options.console_config_path, and the TypeScript builder exposes .consoleConfig("config/console.toml").
The config is normalized and projected through GET /console/experience as console_config.
Agent grouping selectors and badge fields support
labels.<key>, label:<key>, raw label keys, and direct fields such as group, subgroup, role, kind, identity, member_id, and agent_id.
Host-provided attention items
[[attention.sources]] names same-origin JSON endpoints that list things waiting for the operator which the host, not Meta gating, owns. Each source has an id, a short label (shown on its cards and when it cannot be read) and a url. All three keys are required: a missing key fails the console configuration. An empty id or label, or a url that breaks the rules below, drops that source. The url follows the operator panel path rules: one leading /, no scheme or host, no whitespace, control characters or backslashes, and no . or .. segment. A source that breaks a rule is dropped. A realm overlay’s attention list replaces the default list.
The console reads every source with the operator’s session (credentials: "same-origin", redirects refused) when it loads, every 30 seconds while visible, and when the page becomes visible again. A source answers:
id,titleandhrefare required.hrefmust be a same-origin path likeurl; an item with any otherhrefis dropped.detailsis optional, at most 12 label/value pairs.toneis optional:attention,info,warningorcritical.- Text is trimmed and cut at 300 characters, and the console keeps at most 50 items per source.
- A source that does not answer within 10 seconds, or whose answer is larger than 256 KiB, fails alone. A slow source delays the other sources’ items by at most those 10 seconds.
href top-level; a single pending approval is still opened directly. A console without a gating surface (an aggregate console) shows only the host items there. The console never approves, rejects or changes an item: the host page decides. A source that fails (network error, non-2xx answer, malformed body) is named in the inbox as Could not read: <label> and hides nothing else. Gating approvals do not depend on any source.
A source may also name decide_href, a same-origin path where the host decides an approval that this console session cannot (a read-only session). On such a session each pending approval card, in the Approvals inbox and in an agent’s chat, shows one Decide in link per source that names decide_href, named after the source label and loaded top-level, instead of the disabled decision buttons. A settled or expired card shows no link. {decision_id} in decide_href is replaced by the request’s id, URL-encoded once, so decide_href = "/host/decide?decision={decision_id}" opens that request on the host page; without the placeholder the link is the host’s page in general. A decide_href that is not a same-origin path, before or after the id is filled in, is dropped and the source’s items stay. A read-only session with no usable link keeps the disabled buttons.
ConsoleUiConfig is intentionally view-level. It controls presentation, ordering, labels, default pins, and links; it does not decide runtime authorization, routing, or whether an agent exists.AuthPolicy
AuthProvider
ConsolePolicy
For standalone
meta_gateway launches, pass console_read_only: true in the
meta/init params or set META_CONSOLE_READ_ONLY=true. This setting is
part of the gateway resume fingerprint, so toggling it creates or resumes the
matching runtime instead of reusing a runtime launched with the opposite policy.
meta_gateway takes the HTTP exposure knobs as top-level meta/init
params rather than runtime_options: http_listen (HOST:PORT, default
127.0.0.1:0; also --http-listen or META_HTTP_LISTEN_ADDR, the init
param winning), allow_remote (also --allow-remote or
META_HTTP_ALLOW_REMOTE=1) and http_public_base_url. Its protected console
surfaces are open by default when top-level auth_config is absent. Supplying
auth_config installs the shared JWT/OIDC configuration and requires app auth
on those served surfaces. Unlike rpc_gateway’s nested
runtime_options.auth_config, this is a top-level parameter. So is
provider_credentials_root, the provider credentials directory of every
agent the gateway builds (see Provider credentials):
an empty or relative root is refused when the params are parsed, with
-32602 and a null id, a root that cannot be opened refuses init with
-32014 on the request id, and the root joins the resume key.
The standalone gateway’s pre-bootstrap exposure gate currently checks the open
default decision state, before that auth overlay is applied. A non-loopback
http_listen therefore still requires allow_remote, even with configured
auth. The following init-param refusals on this binary (http_listen parse,
the exposure gate, http_public_base_url, meerkat_config_path, compaction,
a [self_hosted] table in mob.toml) answer -32602 on the request id, and
a listener bind failure -32603 on the request id. The listen address and the
advertised base both join the resume fingerprint like --control-listen, so a
relaunch that changes either creates a runtime with the declared values instead
of reporting the previous launch’s. On rpc_gateway, top-level http_listen,
allow_remote, http_public_base_url, meerkat_config_path and
provider_credentials_root are refused with -32602 naming their
runtime_options.* home. See
Deployment.
Both gateways also take --allow-flow-editor-host-deploy (on rpc_gateway
with --persistent). Without an enforced access controller, the Flow Editor
on the gateway’s listener refuses deploys with execute: true and validation
with rkat_validate: true, because both run rkat on the host; the flag
allows them for every caller the route admits. It is launch-only, and on
meta_gateway it joins the resume fingerprint. Rust hosts use
allow_flow_editor_host_deploy(true) on UnifiedRuntimeBuilder or
UnifiedRuntime. See Flow Editor.
Console auth by surface
ConsolePolicy::default().require_app_auth is true, and the SDK gateway’s
decision state without runtime_options.auth_config trusts no signing key, so
its protected console APIs are closed to every caller (each request answers 401
missing_credentials or a token failure). The standalone gateway instead uses
an open default until top-level auth_config is supplied:
An open console is a deployment decision. Use it only behind a loopback bind
or a reverse proxy that authenticates; for anything shared, configure
auth_config (Python .auth(...), TypeScript .auth(...)) instead.
RuntimeOpsPolicy
LocalJsonMemoryBackendConfig
For SDK builds that boot through
rpc_gateway, state_path is derived from .persistent_state(path) as <path>/memory-ledger-state.json; callers provide only backend: "local_json" (and optionally health_check_endpoint). MemoryBackendConfig has no other variant: the predecessor’s backend shape was removed in 0.9 with no alias, so serialized configuration that still names its kind fails to decode and must be rewritten as kind = "local_json". The ledger file itself holds only assertions and conflict signals, with no backend field, so adoption carries it over unchanged. Real external memory hub integration is the wire-boundary provider described in docs/design/memory-hub-roadmap.md.
Ownership of the assertion ledger. The file is a snapshot of runtime
state, not a log that other writers may append to. MetaRuntimeHandle loads
it exactly once at bootstrap and keeps assertions and conflicts in memory as
the authority; every meta/memory/index (host calls, and the Steward’s
conflict signals when agent_memory.steward is enabled) serializes the entire
in-memory state and atomically replaces the file, with no re-read, lock or
modification check. A direct edit is therefore invisible until the next boot
and overwritten by the next flush; single-writer is a convention Meta does
not enforce with a lock. Retention is capped at 4,096 assertions, oldest
evicted first, at index time and at load. At load, a structurally malformed
file (an entry missing assertion_id, entity, topic, store, fact or
indexed_at_ms) refuses the boot with MetaRuntimeError::MemoryBackend,
while a well-formed entry whose entity, topic or store is not canonical, or
whose fact is empty, is dropped without a diagnostic. Append through
meta/memory/index, Python MobHandle.memory_index(...), TypeScript
handle.memoryIndex(...), or Rust MetaRuntimeHandle::memory_index /
UnifiedRuntime::memory_index; there is no agent-facing tool, so a host that
wants members to write exposes one over the handle. This ledger is distinct
from the agent_memory injection and turn-surface ledgers below.
Agent memory injection
agent_memory is separate from meta/memory/*. It is hot identity-scoped memory plus prompt injection: during create/resume/materialize and eligible identity-first turns, Meta asks an AgentMemoryProvider for memories keyed by AgentIdentity and injects a bounded, quoted observation summary.
Generic SDK/work delivery to autonomous_host still skips ambient per-turn
recall; use runtime_mode = "turn_driven" when those turns need it. Non-steer
local canonical console-human input follows a separate preparation path and
can carry configured typed injected context for autonomous_host too. This
exception does not cover arbitrary external bindings, and steer input bypasses
injection in either mode.
The bundled gateway provider is one SQLite database per realm under <persistent_state>/agent-memory (store: "sqlite", the only live gateway store). The earlier per-identity markdown files are import-only: store: "markdown" is refused at init with a migration verdict, and pointing SQLite at the same directory imports a realm’s un-imported markdown files when that realm is first accessed and its SQLite connection is opened, preserving ids, tag content and timestamps and renaming each original <file>.imported rather than deleting it (tags are collated on insert, so tag ORDER is not preserved - assert on the set, not the sequence). The path is server-owned and cannot be overridden from SDK init input. An external memory hub remains an optional deeper knowledge backend for graph/document/truth/provenance use cases and is not required for this hot path.
With the SQLite store, build-time injection also composes a metadata index (record ids, kinds, titles, descriptions, human-phrased age) over the identity’s readable scopes, budgeted at ~8 KiB, ahead of the selected record bodies. Every injected record is logged to an injection ledger inside the realm database and counted in per-record usage statistics; explicit meta/agent_memory/recall reads are counted separately from ambient injection.
Inbound anti-spoofing is on by default (defang_inbound: true): every non-steer identity-first send is scanned for reserved memory-envelope markers (the injection header pattern, <meta_memory_observation> tags, [mem-token: prefixes) and matches are visibly neutralized before delivery, so a peer message or echoed web content cannot impersonate remembered context. A warning with a hit count is logged whenever defanging fires. Steer sends bypass injection and defanging entirely.
Rust builders can use .persistent_agent_memory_stack(config, engines) with .persistent_state(path) to install the full SQLite stack (store plus taint firewall plus the enabled judgment-plane engines). Callers that need a different authority use .agent_memory(provider, config). The former .persistent_agent_memory(...) markdown constructor is removed; legacy markdown is accepted only by the one-shot SQLite importer.
Gateway callers can write records with meta/agent_memory/remember, read them with meta/agent_memory/recall, and delete individual records with meta/agent_memory/forget. TypeScript callers can use handle.rememberAgentMemory(identity, { title, body, tags, realm }), handle.recallAgentMemory(identity, { realm, selection, queryText, queryTerms, maxEntries }), and handle.forgetAgentMemory(identity, memoryId, { realm }); Python callers can use handle.remember_agent_memory(identity, title=..., body=..., tags=..., realm=...), handle.recall_agent_memory(identity, realm=..., selection=..., query_text=..., query_terms=..., max_entries=...), and handle.forget_agent_memory(identity, memory_id, realm=...). Writes and deletes are identity-scoped and require the identity to be registered in the identity runtime. Writes are eligible for turn context on the next identity-first send, subject to the runtime-mode and recall policies, plus build context on materialization/resume/respawn/reset for that identity. A successful write means the configured hot provider accepted the record; it does not mean optional external memory hub extraction/enrichment has completed. Bundled SQLite writes enforce the staged-memory validator’s record limits. The retired markdown parser is used only while importing existing files.
When selection = "always", the latest memories for the identity are injected. When selection = "contextual" (the default), build-time injection sends synthesized identity/profile/label query text and deterministic terms; per-turn injection sends the raw current identity-first message plus normalized terms. Common stopwords are ignored by the bundled provider and records below the relevance threshold are not injected. Meta does not send app context or customizer-added instructions to the memory provider. Semantic/vector retrieval remains a provider concern rather than a Meta runtime authority.
Per-turn injection is governed by per_turn_injection. When omitted, the gateway derives the effective policy from the selected memory backend’s library default: "budgeted" for the bundled SQLite store and "off" for the legacy, import-only markdown kind. Set "off" explicitly to disable ambient push. Selected memories travel as typed injected context immediately before the conversational input, rather than being fused into the user message, and Meerkat excludes that role from semantic-memory indexing. Budgeted injection applies an injection ladder: at most ~4 KiB rendered per record, 20 KiB per assembly, and 60 KiB cumulative per session key between compaction resets, with records offered at most once in that interval (cross-turn dedup). The turn-surface ledger records that preparation-time offer; the transcript’s typed injected-context append records delivery at activation. Build-time injection always applies the per-record and per-assembly caps.
The rpc_gateway memory stack forwards session CompactionCompleted events
through CompactionResetSink to clear both cumulative byte accounting and
cross-turn dedup for that session. After that reset, records compacted out of
context may be offered again in the same session; neither limit is a
lifetime-per-session guarantee.
Native/custom compositions need equivalent reset wiring from
CompactionResetSink to AgentMemoryRuntimeInjector::on_session_compacted.
The current standard Rust .persistent_agent_memory_stack(...) builder does
not install this reset sink, so using that builder alone does not reset session
accounting on compaction.
Automatic build/turn injection uses recall_timeout_ms (default 500, max 30000) and recall_failure_policy (default "skip", or "fail"). With the default skip policy, provider errors and timeouts omit the memory block and allow delivery to continue. Explicit meta/agent_memory/recall requests still return provider errors.
Agent memory is identity-scoped and is not automatically cleared by session respawn or identity reset. Apps that require privacy reset semantics can call meta/agent_memory/forget for specific records or use an AgentMemoryProvider whose lifecycle policy clears memory alongside the app’s identity lifecycle. forget removes records from future recall and injection; it does not erase text already delivered into an active model context, so reset or respawn the identity after deletion when immediate context revocation matters.
BigQueryNaming
ReleaseMetadata
build_runtime_decision_state parses and validates this document on every
build, with or without console auth; nothing reads it at serve time, so the
check is release governance, not runtime policy. RuntimeDecisionState::local_console
fills the canonical value. A malformed document is a DecisionPolicyError::TomlParse
whose message names the release metadata JSON.
Trust manifest (meta.toml)
Constants
See also
- Modules — module configuration in context
- Authentication — auth policy usage
- Sessions — session store configuration
