> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Roster and member lifecycle

> Understand Meta's lightweight roster projection, deep member status, lifecycle states, and mutation results.

*Mirrored from Meerkat Meta main at [26f5ae9da1fd](https://github.com/lukacf/meerkat/commit/26f5ae9da1fd3caac378f1cd847ce50f9ec5ac92) 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`.

> [!WARNING]
>
> 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](#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](#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`:

```python theme={null}
import tomllib

from meerkat_meta import DurableAgentSpec

DEFINITION = tomllib.loads(MOB_TOML)  # the text passed to .mob_inline(MOB_TOML)


def orchestrator_profile(definition):
    # `[mob] orchestrator = "lead"` or `[mob.orchestrator] profile = "lead"`
    orchestrator = definition["mob"].get("orchestrator")
    return orchestrator.get("profile") if isinstance(orchestrator, dict) else orchestrator


class ProfileShapedRoster:
    # One durable identity per profile; the orchestrator is addressable.
    async def roster(self, context):
        orchestrator = orchestrator_profile(DEFINITION)
        return [
            DurableAgentSpec(
                identity=f"{profile_name}:main",
                profile=profile_name,
                addressability=(
                    "addressable" if profile_name == orchestrator else "internal_only"
                ),
            )
            for profile_name in DEFINITION.get("profiles", {})
        ]
```

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.

| Method | Result | Description |
| - | - | - |
| `meta/list_members` | `MobMemberListEntry[]` | List operationally visible members, including retiring members |
| `meta/get_member` | `MobMemberListEntry` | Get one lightweight roster entry by member ID |
| `meta/find_members` | `MobMemberListEntry[]` | Find lightweight roster entries by a label key-value pair |
| `meta/ensure_member` | Gateway-profile dependent | Ensure a durable identity or an ephemeral worker as described below |
| `meta/member_status` | `MobMemberSnapshot` | Inspect one member's deep execution and connectivity state |
| `meta/retire_member` | Acceptance acknowledgement | Retire a member |
| `meta/respawn_member` | Acceptance acknowledgement | Replace a member with a fresh runtime incarnation (identity-first: resets the identity onto a fresh session at a new generation) |
| `meta/reload_member` | `{reloaded, disposition, session_id, generation}` | Non-destructive cold reload: same identity, same durable session, same generation (identity-first only) |
| `meta/member_health` | `MemberHealthReport` | Lifecycle and delivery health from in-process reads; answers while the mob actor loop is stalled |

## `ensure_member` gateway profiles

The same method name has different ownership and result contracts across the
two gateway profiles:

| Gateway profile | Behavior | Result |
| - | - | - |
| SDK-facing `rpc_gateway` (`StdioRpc`) | Calls `MobHandle::ensure_member` on the ephemeral worker plane | `MobMemberListEntry`, or `null` if the post-ensure roster lookup finds no entry |
| Console/admin `meta_gateway` with identity-first runtime and roster | By default, upserts a durable identity into the desired roster and reconciles it through the identity runtime | Identity receipt with `agent_identity`, `role`, `identity_first: true`, `outcome`, and `state` |
| Identity-first console/admin request with `plane: "worker"` | Explicitly uses `MobHandle::ensure_member` on the ephemeral worker plane | `MobMemberListEntry`, or `null` if the post-ensure roster lookup finds no entry |

> [!WARNING]
>
> 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:

```json theme={null}
{
  "agent_identity": "agent-1",
  "role": "worker",
  "runtime_mode": "turn_driven",
  "wired_to": ["agent-2", "agent-3"],
  "labels": {"owner": "user-123"},
  "status": "active",
  "state": "active",
  "is_final": false
}
```

| Field | Type | Description |
| - | - | - |
| `agent_identity` | `string` | Canonical public identity used to address the member |
| `role` | `string` | Role or profile name used to build the member |
| `runtime_mode` | `string` | `autonomous_host` or `turn_driven`; see [Runtime modes](#runtime-modes) for the default and its consequences |
| `wired_to` | `string[]` | Canonical identities of currently wired peers |
| `labels` | `object` | String key-value metadata |
| `status` | `string` | Machine-projected lifecycle status |
| `error` | `string`, optional | Machine-recorded member restore failure; omitted when absent |
| `is_final` | `boolean` | Whether the member has reached a terminal state |
| `kickoff` | `object`, optional | Initial autonomous-turn kickoff state, when present |
| `state` | `string` | Meta compatibility alias of `status`; this is not a field on the Rust `MobMemberListEntry` |

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):

```toml theme={null}
[profiles.worker]
model = "gpt-5.5"
runtime_mode = "turn_driven"
durable = true
```

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](https://docs.rkat.ai/meta/reference/configuration.md#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](https://docs.rkat.ai/meta/reference/configuration.md#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](https://docs.rkat.ai/meta/guides/unified-runtime.md#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:

| Status | SDK constant | Meaning |
| - | - | - |
| `active` | `MEMBER_STATE_ACTIVE` | Active and potentially running |
| `retiring` | `MEMBER_STATE_RETIRING` | Retirement is in progress |
| `broken` | `MEMBER_STATE_BROKEN` | Durable session restore failed and the member needs repair |
| `completed` | `MEMBER_STATE_COMPLETED` | The session was archived or not found |
| `unknown` | `MEMBER_STATE_UNKNOWN` | The member is not in the roster |

> [!NOTE]
>
> `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:

```json theme={null}
"role_migrations": [{"identity": "domain:automation", "from_role": "domain"}]
```

Python hosts declare the same thing on the builder, with
`RoleMigrationDeclaration` dataclasses or plain
`{"identity": ..., "from_role": ...}` dicts:

```python theme={null}
from meerkat_meta import RoleMigrationDeclaration

builder.role_migrations([
    RoleMigrationDeclaration(identity="domain:automation", from_role="domain"),
])
```

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.

> [!WARNING]
>
> 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`.

> [tab] Python
>
> ```python theme={null}
> from meerkat_meta import (
>     MEMBER_STATE_ACTIVE,
>     MEMBER_STATE_BROKEN,
>     MEMBER_STATE_COMPLETED,
>     MEMBER_STATE_RETIRING,
>     MEMBER_STATE_UNKNOWN,
> )
>
> known_states = {
>     MEMBER_STATE_ACTIVE,
>     MEMBER_STATE_RETIRING,
>     MEMBER_STATE_BROKEN,
>     MEMBER_STATE_COMPLETED,
>     MEMBER_STATE_UNKNOWN,
> }
>
> members = await handle.list_members()
> for member in members:
>     if member.state not in known_states:
>         # A newer gateway may add a status value.
>         continue
>
> detail = await handle.member_status("agent-1")
> print(detail.status, detail.is_final, detail.error)
>
> await handle.retire_member("agent-1")  # Returns None.
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> import {
>   MEMBER_STATE_ACTIVE,
>   MEMBER_STATE_BROKEN,
>   MEMBER_STATE_COMPLETED,
>   MEMBER_STATE_RETIRING,
>   MEMBER_STATE_UNKNOWN,
> } from "@rkat/meta-sdk";
>
> const knownStates = new Set<string>([
>   MEMBER_STATE_ACTIVE,
>   MEMBER_STATE_RETIRING,
>   MEMBER_STATE_BROKEN,
>   MEMBER_STATE_COMPLETED,
>   MEMBER_STATE_UNKNOWN,
> ]);
>
> const members = await handle.listMembers();
> for (const member of members) {
>   if (!knownStates.has(member.state)) {
>     // A newer gateway may add a status value.
>     continue;
>   }
> }
>
> const detail = await handle.memberStatus("agent-1");
> console.log(detail.status, detail.isFinal, detail.error);
>
> await handle.respawnMember("agent-1"); // Returns void.
> ```

## Reconciliation

Reconciliation compares the desired member set with the current roster and
spawns, retains, or retires members to converge. See the
[unified runtime guide](https://docs.rkat.ai/meta/guides/unified-runtime.md) 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):

```json theme={null}
{
  "context": {
    "mob_definition": { "id": "team", "profiles": { "...": {} } },
    "previous_identities": ["identity:operator", "triage:main"]
  }
}
```

| Field | Meaning |
| - | - |
| `mob_definition` | The booted mob's compiled definition. Always present behind the bundled `rpc_gateway`; `null` only when an in-process Rust embedder or test harness resolves a roster without a mob definition. |
| `previous_identities` | Call-site-dependent prior-identity context, not an authoritative membership snapshot on every callback. Topology-query and edge-reconciliation snapshots supply currently registered identities. Bootstrap, full roster refresh/reconcile, and reset-time spec adoption currently pass an empty list; an empty list does not establish that no identities are registered. |

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

* [Identity-first doctrine](https://github.com/lukacf/meerkat-meta/blob/main/docs/design/identity-first-doctrine.md) - choose the identity plane or worker plane
* [Python SDK](https://docs.rkat.ai/meta/sdks/python.md) - Python roster helpers
* [TypeScript SDK](https://docs.rkat.ai/meta/sdks/typescript.md) - TypeScript roster helpers
* [RPC API](https://docs.rkat.ai/meta/api/rpc.md) - JSON-RPC protocol and method signatures
* [Unified runtime guide](https://docs.rkat.ai/meta/guides/unified-runtime.md) - reconciliation and session lifecycle


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.