Skip to main content
Mirrored from Meerkat Meta main at 26f5ae9da1fd on 2026-11-03; Meta version 0.9.9 at that commit. The mob roster is the operational view of the members known to a mob. Roster reads return lightweight MobMemberListEntry projections. Deep execution inspection is a separate operation that returns MobMemberSnapshot.
Member lifecycle is dual-plane. Durable members the host owns, including per-user agents and long-lived coordinators, belong on the identity plane: declare them in an identity roster and reconcile them. The worker plane holds the members agents create at runtime: helpers reaped by an idle-retire policy, and long-lived members spawned from a durable = true profile, which survive a restart (see Long-lived members an agent creates). An agent never creates an identity. The gateway profile changes the default behavior of meta/ensure_member; see the profile-specific contract below.

Profiles are templates and members are declared

A [profiles.<name>] entry in mob.toml is a role template: model, tool posture, skills, runtime mode, peer description. It produces no member. A member exists only when something names it:
  • the identity roster: the RosterProvider callback on rpc_gateway (Python .roster(provider), TypeScript .rosterProvider(provider)), or the identity_roster top-level meta/init param on meta_gateway (a list of DurableAgentSpec objects, restored at boot and extended at runtime by meta/ensure_member);
  • a Rust Discovery implementation on UnifiedRuntimeBuilder (classic, non-identity boot only; neither gateway binary wires one);
  • an explicit meta/ensure_member.
Nothing derives one member per profile, and neither does meerkat: a mob definition’s orchestrator names a profile, not a member. The only automatic spawn anywhere is meta_gateway’s no-config fallback, which ensures a single alpha member of its built-in definition when no config/mob.toml or definition.json exists; a workspace definition with an empty identity_roster boots with no members at all. On rpc_gateway, callback/roster_provider/roster supplies the serialized RosterContext under params.context. Both SDKs pass a typed context to roster(context): read context.mob_definition in Python or context.mobDefinition in TypeScript for the booted mob’s compiled definition. Its id, orchestrator, and profiles are top-level fields, unlike the [mob] nesting in the source TOML. Prior-identity context is call-site-dependent; an empty list does not establish that no identities are registered. See Roster provider context for the field contract. A host may instead derive a profile-shaped crew from its own parsed mob.toml as an explicit policy choice, not as a workaround for a missing callback definition. This example uses the same text passed to .mob_inline(MOB_TOML) and intentionally ignores context:
Profile-to-member is not one-to-one in general (a per-user profile may back hundreds of identities), which is why this stays a host decision rather than a default.

When to call reconcile_edges

Declared definition wiring ([wiring] auto_wire_orchestrator = true or role_wiring rules) is applied by meerkat only at spawn time and, for the orchestrator rule, only from the non-orchestrator side, so the result depends on bring-up order. Meta installs the definition’s wiring as a reconcilable edge policy and converges it with meta/reconcile_edges (Python handle.reconcile_edges(), TypeScript handle.reconcileEdges()).
  • meta/ensure_member already runs that reconcile after every successful ensure, on both the worker plane and the console identity plane. Calling reconcile_edges after ensure_member is redundant.
  • meta/reconcile_identity (Python runtime.reconcile()) and the identity-first gateway boot do not: the gateway reconciles definition edges before the roster is materialized. When the definition declares wiring, call reconcile_edges once after boot and after each runtime.reconcile().
  • Without declared wiring there is no definition edge policy; the call returns an empty report. Edges from a host TopologyProvider converge on every identity restore and reconcile on their own.
  • With a TopologyProvider, the provider (plus operator topology intent) owns the identities’ edges: reconcile unwires every live edge between two identities that neither the provider nor the definition’s wiring declares, including one wired before a restart. Declare an edge in your provider to keep it. Without a provider, reconcile unwires only the edges it wired itself.

Roster operations

All operations are available through the gateway’s JSON-RPC interface.

ensure_member gateway profiles

The same method name has different ownership and result contracts across the two gateway profiles:
To create an ephemeral worker through an identity-first console/admin gateway, you must pass plane: "worker". Omitting it creates or retains a durable identity and returns an identity receipt, not a MobMemberListEntry.
The identity receipt’s outcome is created, resumed, dormant, refused or unchanged. The other roster operations keep the contracts shown in the operation table.

Lightweight roster entries

list_members, get_member, and find_members project Meerkat’s MobMemberListEntry. ensure_member projects the same type only on the worker-plane paths described above. A raw Meta worker-plane response has this shape:
The lightweight entry deliberately omits session IDs and live connectivity fanout. Use meta/member_status when those details are required.

Runtime modes

runtime_mode comes from the profile ([profiles.<name>] runtime_mode), or from a per-spawn override where a surface offers one. autonomous_host is meerkat’s default when a profile omits the key, and it is the mode most hosts end up on without choosing it. The two modes differ in ways that show up on the roster and on every send:
  • A fresh autonomous_host member runs one kickoff turn as soon as it is spawned, on its initial_message if the spawn carried one, otherwise on meerkat’s fallback prompt (You have been spawned as '<identity>' (role: <role>) in mob '<mob>'.). The kickoff field on the roster entry reports that turn. Later sends are ordinary durable inputs that queue behind it; nothing is dropped, but the member answers in order.
  • Autonomous inbox delivery carries no user-channel work boundary, so meerkat refuses injected context and completion-bearing sends on that path. Meta therefore skips ambient per-turn agent-memory recall for autonomous_host members with the typed reason runtime_mode_autonomous_host (the turn still runs, without memory). Ambient per-turn memory requires runtime_mode = "turn_driven" until meerkat’s carrier change lands; the Unreleased entry in the repository CHANGELOG.md records this.
  • turn_driven members run exactly one turn per delivered input and sit idle between inputs. It is the mode meerkat’s tracked, completion-bearing turn carriers support; on autonomous_host those are refused with tracked turn completion is not supported by autonomous inbox delivery.
Pin runtime_mode = "turn_driven" explicitly on every profile that hosts a durable, addressable identity unless the member is meant to run its own inbox loop. Operational list projections retain active, retiring, and broken rows as applicable. They filter terminal completed and unknown rows, even though those values remain part of the status vocabulary.

Durable workers

A profile can declare durable = true (meerkat 0.9.5):
The flag belongs to the profile alone. DurableAgentSpec, meta/ensure_member and the SDKs take no durable argument, and an omitted key means an ordinary profile.
  • Meerkat spawns the profile’s members with a durable identity, keyed by the member’s identity, so a host can list and re-attach them after a restart. Every identity-first member is already spawned that way, so for an identity the flag changes nothing in how Meta creates, wakes or resets it.
  • A durable profile needs a mob whose member sessions outlive the process: a runtime built with persistent_state or a storage provider. On any other mob Meerkat refuses the spawn before anything is created, whether the profile is inline or a realm profile: MobError::NonDurableService, code CAPABILITY_UNAVAILABLE (-32020), data.kind = "mob_non_durable_service" with the member_id and the profile. That includes Meta’s default composition: without persistent_state or a storage provider its member sessions live in memory, so it answers Meerkat’s keeps_durable_member_sessions false.
    • On the identity plane the refusal is a deterministic build refusal. The wake answers HostRejectedBuild with a reason that names the remedy: run the mob on a persistent realm, or drop durable from the profile. Nothing retries it and nothing is recorded; every wake answers it again.
    • On the worker plane, meta/ensure_member answers Meerkat’s own code and data instead of the generic -32602.
  • The mobpack editor document carries the flag as the member key durable, present only when true. The export writes durable = true back into the profile, and update_member accepts a boolean durable patch. Validation refuses a member whose durable disagrees with mob.toml (editor_profile_durable_mismatch) or is not a boolean (invalid_durable).

Resuming a durable worker’s run

Meerkat rebuilds a durable worker on its session after a crash, kill or restart, and continues the run it was in from its last committed round. Meerkat keeps no WorkGraph claim alive while the worker is down, so a worker that was down longer than its lease can come back to work that has moved on. Before such a run continues, Meerkat asks the mob’s resume check. It asks for every member spawned with a durable identity: the members of a durable = true profile, and every identity-first member, whose spawn always carries one. When the mob runtime is composed with a WorkGraph service, Meta answers the check from that WorkGraph:
  • A run dispatched for an item (its input carries the WorkGraph attention projection) continues only while the item lives in the mob’s realm and is live, unclaimed or claimed by the worker under a lease that has not lapsed, and while its attention binding is active or paused and still targets the worker.
  • A run that named no item continues unless a claim the worker holds has lapsed. A worker that holds no claim continues.
  • Otherwise the run is superseded. Meerkat settles it the way a stop does: its bound inputs are abandoned as refused resumes, the run ends failed with the check’s reason, and it never resumes. A lapsed lease is committed first, with the WorkGraph’s lease-expiry observation, so the item can be claimed again, and every later attempt of the same resume answers superseded too.
  • When the WorkGraph cannot be read, or the run’s attention projection does not decode, the check answers unavailable. Meerkat asks again, up to three attempts, then settles the run the same way.
The check knows the worker by its member owner key (agent:mob/<mob_id>/agent/<identity>) and by its session (session:<id>); a claim under any other key belongs to another owner. Two limits apply. A run that named no item, whose lapsed claim another worker took while it was down, holds no claim any more, so the check cannot see the move and the run continues; its later WorkGraph writes are revision-checked and conflict. And the mobs that agents create through the mob tools have no check: their durable workers continue unchecked.

Long-lived members an agent creates

An agent creates a long-lived agent as a mob member, never as an identity. Identities are declared by the host alone: a roster provider, the identity_roster init param, or meta/ensure_member. The agent spawns the member from a durable = true profile, into its own mob (mob_spawn_member, or the member tool spawn_member) or into a child mob it creates (mob_create, then mob_spawn_member).
  • It survives a restart as a member of its mob. The roster pass retires only the identities the roster materialized, so a member an agent spawned is never retired because no roster lists it. It ends through its own lifecycle: its work’s completion or supersession, an explicit retire, or the idle-retirement policy when the spawn opted in with idle_retire_secs (see Implicit delegate idle retirement). After a restart it is restored like every member of its mob: a member of the root mob is materialized at start only as member_restore says (see SDK and gateway runtime options), and otherwise on its first message or turn.
  • Talking to it. A member an agent spawns into its own mob with mob_spawn_member is wired to it by default: Meta sets auto_wire_parent when the agent omits it, so the two talk through comms, before and after a restart. The member tool spawn_member wires the two only when the agent passes auto_wire_parent: true. The host reaches the member by its member id with meta/send_message.
  • On a host store realm child mobs are process-bound and do not survive a restart, while members of the realm’s root mob do (see Keeping a Meta realm in your own database). There, an agent spawns its long-lived members into its own mob.
  • What it lacks compared with an identity:
    • identity status: meta/status_identity, meta/inspect_identity and the identity bootstrap report do not know it;
    • a Console identity: it is not among the Console’s identities (/console/identities), and the identity-addressed meta/console/send does not reach it (the Console still shows its spawn and its timeline);
    • agent memory, which is identity-scoped: no recall injection and no memory tool;
    • identity delivery: meta/send, meta/interact and meta/dispatch, with their completion baselines, turn tickets and keyed sends, address identities only.

Deep member status

meta/member_status serializes Meerkat’s MobMemberSnapshot. It is a deep inspection result, not the type returned by roster list operations. Its core fields are status, output_preview, error, tokens_used, is_final, and current_session_id. It can also include peer connectivity, kickoff, external-member observations, resolved model capabilities, execution progress, placement, reachability, freshness, and lifecycle-capability diagnostics. When Meerkat could not observe the member’s session for the status (meerkat 0.8.45+), preview_unavailable names why (observation_deadline, read_failed, session_absent, not_observed_while_retiring), and a missing output_preview then does not mean an empty member. meta/inspect_identity and the console’s per-member progress carry the same marker. The snapshot’s Rust agent_identity is bridge-internal and skipped during serialization. The requested member ID identifies the result on the Meta RPC surface. The snapshot also does not carry the roster entry’s role, runtime_mode, wired_to, or labels fields.

Member statuses

The known MobMemberStatus values are:
MobMemberStatus is non-exhaustive. Branch on the known values and tolerate new status strings. Do not reduce lifecycle handling to only active and retiring. The Rust MobMemberSnapshot structure is also non-exhaustive.
retiring can be transient. At the Meerkat Rust layer, a successful MobHandle::retire is the terminal lifecycle barrier and returns unit, so a later roster read can show the member as absent rather than retiring.

Role migration

A durable member’s role is part of its durable identity, not a configuration knob: the stored role, the member’s comms name, and its mob-member binding are one fact. A resume that targets a different role than the durable predecessor is therefore a restamp of identity, and Meerkat refuses it with MobError::MemberRoleMigrationRequired unless that exact resume declares the migration. If a durable member stopped resuming after a role or profile rename, this is the refusal to look for. Migration authority is supplied by the activation, per exact identity, as a top-level meta/init param. It is not a runtime_options field:
Python hosts declare the same thing on the builder, with RoleMigrationDeclaration dataclasses or plain {"identity": ..., "from_role": ...} dicts:
The declaration is boot-scoped and never persisted. It is installed on the session bridge, which lives for exactly one boot, so dropping it from the next boot payload is how the authority goes away. Lookup is an exact identity match: no prefix, suffix, or case variant of a declared identity inherits authority. Meta only carries the declared predecessor role into the resume request. Meerkat re-verifies it against durable state and refuses with MobError::MemberRoleMigrationRejected on mismatch, so a mistyped from_role cannot authorize an unintended restamp, and Meta never retries a resume that Meerkat refused. Once the durable and requested roles agree, Meerkat returns before it reads the declaration at all, so a declaration left in place after the migration landed is inert rather than a repeated restamp. That is why an identical repeated declaration is accepted on purpose, while one identity declared twice with conflicting predecessor roles is refused. The roster pass meets every identity Core holds under the role Core stored for it, at a start and at a roster refresh alike, so a declared migration first meets the member still on its old role. Meta then applies it or holds it, and never adopts the member as it is:
  • When the identity plane has repair custody (no Meerkat identity intent, or an Absent one), Meta retires the member to absence, after proving the session it will resume exists and carrying its queued input, resumes the same session under the declared role, and releases the member again, so its liveness is as before. Meerkat re-verifies the predecessor role, and Meta checks that the member runs the declared role. A member no wake has created a session for yet (seated, never woken) has no session to keep or transcript to lose: the declared move rotates it onto a fresh session under the declared role at the next generation, and its reconcile outcome reads created.
  • When the restored member’s role is not the declared from_role, the declaration does not describe it, and Meta refuses before retiring anything.
  • When a Present Meerkat identity intent (from mob/adopt_member_identity_declaration) owns the member, the intent pins its profile and MobMachine owns its materialization, so Meta refuses rather than retire it. Declare the new profile in the identity intent.
A profile change this activation declared no migration for, or a refused migration, is held with the typed kind identity_role_migration_refused (-32603), and the member is never retired for it: the identity stays a member of Core’s mob under its previous role and reads active (an identity Core holds no member for stays unseated and reads dormant), its wake_refusal names the remedy resolve_role_migration, a delivery through Meta is refused with that kind, and the meta/init result reports it under identity_bootstrap (restore stage refused with that kind), so a held migration is visible at boot. A roster refresh that moves an identity to another profile releases the identity’s live member and then plans the same way: it no longer retires the member, so the identity never leaves the mob and keeps its session and generation. Core still holds the member under its previous role, so a peer message Core routes to it directly wakes it under that role. No pass retires the member the hold kept; each roster pass and each delivery through Meta checks again, so declaring the migration (next boot) or restoring the roster spec ends the hold. On a runtime whose sessions live in memory only (one built with MobBootstrapSpec::ephemeral, whose session service keeps no session beyond its member), there is no session to carry over to the new role, and Core’s release keeps such a member materialized. So a roster pass that moves an identity Core holds to another profile neither releases it nor holds it: once the member is idle, the pass rotates the identity onto a fresh session under the new profile at the next generation, in one Core step that retires the previous incarnation, whether or not a migration is declared. There is no session continuity across a reprofile in this mode: the next turn, whether a delivery through Meta or a message sent to the member on the mob, runs under the new profile on the fresh session, and the previous role answers nothing more. A member with work in flight (a running turn, or input queued behind it) is not rotated, so none of that work is cancelled: Core refuses the release the pass asks for first, the work completes under the previous role, and a later roster pass, or an explicit materialization or warm of the identity, rotates it once it is idle. Inputs sent through Meta wait for the move; an input that reaches the member outside Meta (a peer message, a send on the mob handle) in the moment between Core’s idle answer and the start of the retirement ends, typed, with the previous incarnation. A rotation Core refuses (another lifecycle operation of the member is in flight) waits the same way. Until then, deliveries through Meta and messages from peers still run under the previous role; a delivery does not retry the move. A host that composes its session bridge without the mob’s session service (MobSessionBridge::new) cannot tell that the sessions live in memory only, so it keeps the release and the hold described above, under which Core keeps the member materialized under its previous role; compose it with MobSessionBridge::with_session_service, as the gateways do.
A malformed or self-contradicting role_migrations payload refuses the boot rather than arming nothing, and the two gateway binaries refuse differently. rpc_gateway parses and checks the key at init scope and answers both refusals with -32602 on the request id. meta_gateway answers with a null id in both cases: -32602 when its typed init params fail to deserialize, and -32603 from the conflict check that sits inside its identity-first block. Under identity_first: false that block never runs, so a self-contradicting payload is then not refused at all; identity_first defaults to true. The Python builder raises ValueError on a conflicting pair before any of this reaches a gateway.
Only the identity plane carries migration authority. Every mob-plane resume path passes no declared predecessor role, so a role change reached through the worker plane still fails closed. Which declarations arm also depends on the binary: rpc_gateway installs them inside its roster-provider branch, meta_gateway inside its identity-first block. Declarations that reach neither arm nothing, because there is no identity plane to migrate on.

Peer identity and comms names

A member’s comms (peer) name is {mob_id}/{role}/{member} (meerkat’s MemberCommsName): the mob definition id, the profile name, and the mob-roster member id. That is the string peers see in the peers tool and in Peer message from ... projections; the routing key underneath is the pubkey-derived peer id. Identity-first members are registered under an encoded roster id, because a durable identity such as domain:security contains : and a comms-name component may only contain ASCII letters, digits, -, and _. Meta’s mk-- codec escapes it (domain:security becomes mk--domain_csecurity), so the peer-facing name is home/domain/mk--domain_csecurity. Console, RPC, and SDK surfaces decode the id back to the public identity; the model-facing projections do not. The mob id is part of every member’s name, so renaming a mob (changing the definition id) re-mints every peer name in it. Nothing rewrites records that already hold the old strings: identity-keyed agent memory lives in the host realm, survives the rename, and can recall {old_mob}/... names into a later prompt. Renaming a member’s role is a different case and is refused unless declared, as the previous section describes.

Mutation results

The underlying Meerkat Rust operations have different success types:
  • MobHandle::retire returns Result<(), MobError>.
  • MobHandle::respawn returns Result<MemberRespawnReceipt, MobRespawnError>. The receipt identifies the respawned member.
The domain MemberRespawnReceipt publicly serializes only identity; its runtime and fence atoms are internal. Meerkat’s typed mob/respawn surface wraps that outcome as status (completed or topology_restore_failed), a receipt containing identity and an opaque member_ref, and optional failed_peer_ids. Meta’s compatibility RPC consumes the worker-plane respawn receipt and projects both mutations as acceptance acknowledgements. Identity-owned targets can add identity-first receipt fields such as a session ID and generation for respawn. The Python and TypeScript SDK wrappers discard these acknowledgement objects and return None or void. Re-read the member when the caller needs the resulting roster or execution state.

SDK projections

The SDK type named MemberSnapshot wraps the lightweight roster response. It currently exposes agent_identity, role, state, wired_to, and labels. It does not expose every field present in the raw MobMemberListEntry JSON. The separate RichMemberSnapshot type wraps meta/member_status.

Reconciliation

Reconciliation compares the desired member set with the current roster and spawns, retains, or retires members to converge. See the unified runtime guide for the complete flow.

Roster provider context

Every time the identity runtime derives the desired roster it calls the host’s roster provider with a RosterContext. Behind the SDK gateway that is the callback/roster_provider/roster request, whose params nest the context under context (the same envelope the topology and customizer callbacks use):
The Python SDK delivers this as RosterContext from meerkat_meta.identity_first_models; the TypeScript SDK as the exported RosterContext interface (mobDefinition, previousIdentities). Use context.mob_definition / context.previous_identities in Python and context.mobDefinition / context.previousIdentities in TypeScript; the prior-identity field has the same call-site qualification in both SDKs.

See also