Skip to main content
Mirrored from Meerkat Meta main at 26f5ae9da1fd on 2026-11-03; Meta version 0.9.9 at that commit. Meta exposes its operational API over JSON-RPC 2.0. SDK gateways use JSON lines over stdio, while the embedded console uses POST /console/rpc for console-specific methods and explicitly implemented runtime operations. The HTTP dispatcher is not a general stdio passthrough; check meta/capabilities on the surface you use.

Protocol

  • SDK transport: JSON lines over stdio (one JSON object per line)
  • Console transport: HTTP POST /console/rpc
  • Version: JSON-RPC 2.0
  • Contract version: 0.5.0

Initialization handshake

The persistent SDK gateway (rpc_gateway --persistent) does not emit an unsolicited capabilities line. It waits for the client, whose first JSON line must be a meta/init request:
On success, the JSON-RPC response carries contract_version in result, together with http_base_url and the gateway’s runtime metadata. Clients that enforce contract compatibility compare result.contract_version before sending later requests. After init, meta/capabilities returns the methods available on that runtime.

Accepted then settled (opt-in)

Startup runs provider callbacks (roster, topology, agent builds), each bounded at 130 seconds by the gateway, and native work such as the identity restore that is deliberately unbounded. A single response written when all of that finishes cannot be bounded by any fixed request timeout, so the SDKs opt in to a two-step protocol:
The SDK generates init_id and starts listening for it before it writes the request. Only the exact protocol name together with a non-empty init_id opts in. Anything else gets the legacy single response, so a client that did not opt in never receives accepted.
  1. accepted. After the cheap pre-durable gates pass (mob TOML, model resolution, host-config tables), the gateway answers the request at once:
    accepted is not readiness and carries no http_base_url. A refusal from those gates is still the plain JSON-RPC error response, and it means nothing past them ran.
  2. meta/init_progress notifications, one per startup phase as it begins: storage (first touch of durable state), owner_publication (the early roster callback), prepare (the identity roster callback, before the mob is built, then runtime and storage prepare and module startup), roster (the roster bootstrap begins with that roster), register_owners, activate, restore, schedules, serve, and cleanup (failure cleanup). Meta 0.9.4 no longer reports prewarm. Progress events are non-terminal, and their order follows the native init order. Callback requests continue to arrive between them.
  3. Exactly one meta/init_settled notification. Only a ready settlement for the same init_id establishes readiness. It carries every field of the legacy init result:
    A failed settlement carries the error that the legacy response would have carried (code, message, optional data) plus durable_effects. The gateway exits after writing it.
    durable_effects is "none" only when the init failed before the storage phase began, which is positive evidence that nothing durable was touched. Every later failure is "possible", including a failure of the cleanup itself. The SDKs raise the same typed errors as before (for example StorageResolutionError for -32014) and add data.durable_effects.
Shutdown during init. meta/shutdown stays admissible while init runs. The gateway stops at the next phase boundary. If a runtime exists, it runs the normal runtime shutdown. Only after that cleanup has settled does it emit the failed settlement (code: -32098, data.reason: "shutdown_requested"). It then answers the shutdown request. Drain window. params.shutdown_in_flight_drain_ms, an integer of milliseconds from 0 to 300000 (absent or 0 is no drain), lets in-flight member turns finish when the gateway stops through the shutdown handshake or SIGTERM. The gateway reads it before it answers, refuses any other value with -32602, and advertises the longer horizon in stdio_shutdown_horizon_ms (in accepted and in the init result): 442000 plus the window. The meta/shutdown answer then carries the drain’s report as result.in_flight_drain. Without a window the shutdown does not wait for running turns: it keeps a durable member’s turn for the member’s next start and cancels any other. See Letting in-flight turns finish on shutdown. Startup callbacks must not issue ordinary RPCs. Ordinary request dispatch starts only after init settles, so the gateway answers an ordinary request that arrives earlier at once with -32018 (data: {kind: "init_in_progress", method}). It does not queue the request: a provider callback that awaited it during init would otherwise wait on itself, and a full queue would stall the routing of callback responses. meta/shutdown and callback responses are never refused. This applies whether or not the SDK opted in to accepted-then-settled. Outcome unknown. Once the init request is written, the SDKs never turn a missing answer into a refusal. A lost acceptance, a reader failure, the gateway exiting before it settles, or the caller’s optional init deadline (Python MetaBuilder.init_deadline(seconds), TypeScript MetaBuilder.initDeadline(ms)) raises InitOutcomeUnknownError(init_id, last_phase, reason). The gateway may already have changed native state, so the SDKs never retry automatically. By default the SDKs wait for the settlement with no deadline. Every callback is still bounded by the gateway, and a callback that runs out settles the init failed.

role_migrations

A durable member whose role changed refuses to resume (MobError::MemberRoleMigrationRequired) until the host declares the migration, so an unintended role edit cannot silently restamp a member’s durable role, comms name and binding. The key sits at the TOP LEVEL of params, a sibling of runtime_options and persistent_state, not inside runtime_options. rpc_gateway reads it at init scope, and its runtime_options handling is a closed allowlist that would reject it as an unsupported field.
Each entry is {identity, from_role}: the exact identity being migrated (no wildcards, no prefix matching) and the role it is migrating FROM. The gateway installs the list on its session bridge for the life of this boot and never persists it, so omitting it next boot removes the authority. Lookup is by exact identity; an identity the host did not name carries no migration authority. Meerkat re-verifies from_role against the durable predecessor role and refuses with MobError::MemberRoleMigrationRejected on mismatch, and ignores the declaration once the roles already agree, so a leftover declaration is inert rather than a repeat restamp. The roster pass applies a declared migration whenever the roster names a profile other than the role Core holds the identity under, at a start and at a roster refresh alike: the member moves to the new role on its own session. A member no wake has created a session for yet (seated, never woken) has no session to keep: the declared move rotates it onto a fresh session under the new role at the next generation, and its reconcile outcome reads created. A profile change this activation declared no migration for is held, never retired: the identity stays a member under its previous role and reads active (one Core holds no member for stays unseated and reads dormant), its wake_refusal names resolve_role_migration, a delivery through Meta is refused (identity_role_migration_refused) until an activation declares the migration, and identity_bootstrap in the meta/init result reports it as restore stage refused with that kind. Core still holds the member under its previous role, so a peer message Core routes to it directly wakes it under that role. See Roster for when Meta applies and when it refuses. A malformed payload fails meta/init with -32602 rather than arming nothing, on both gateway binaries and whether or not an identity plane is configured. One identity declared twice with conflicting from_role values is refused too, rather than letting hash order pick which predecessor role becomes authority, but the refusal differs by binary: rpc_gateway checks at init scope and answers -32602 on the request, while meta_gateway checks inside its identity-first block and fails the boot out as a -32603 internal error with a null id. Under identity_first: false meta_gateway runs neither that check nor the install, so a self-contradicting payload is not refused and nothing is armed. An identical repeat is accepted on purpose. The Python SDK exposes this as MetaBuilder.role_migrations([...]), taking RoleMigrationDeclaration dataclasses or plain dicts, both validated; a conflicting pair raises ValueError, and an omitted or empty list emits no role_migrations key at all. The TypeScript SDK has no equivalent surface.

Request format

Response format

Error format

The optional data field carries structured context for typed errors. SDKs surface it on RpcError.data.

Meta-specific error codes

meta/init refuses the members’ native realm and auth binding (runtime_options.native_realm, runtime_options.member_auth_binding) typed, before any agent factory is built, with error.data.kind: -32602 for native_realm_config_conflict (beside meerkat_config_path), member_auth_binding_required (a native realm without a binding), native_realm_invalid, native_realm_config_invalid (realm documents that do not read or validate), member_auth_binding_invalid, member_auth_binding_origin (an origin other than "configured"), member_auth_binding_unknown (a binding the members’ configuration does not define), member_auth_binding_mismatch (a definition profile the binding does not serve, inline or the realm profile it references) and member_auth_binding_uncarried (an automatic spawn policy, a model fallback target that names another binding or none, image generation, the agent memory engines, identity declarations an earlier launch adopted and sealed off the binding, or a stored child mob whose definition would build members off it), and -32014 for native_realm_unknown and native_realm_unreadable (a realm whose storage cannot be resolved, or launch state these checks cannot read, also on a launch with member_auth_binding and no native realm). None falls back to the default configuration or binding. On such a launch mob/adopt_member_identity_declaration and mob/apply_member_tool_declaration answer -32602 member_auth_binding_uncarried too, before anything is written; meta/attach_existing_session, or meta/ensure_member with a resume_session_id, of a session whose live actor in the gateway’s process runs another binding (Meerkat reuses a live actor as it is) answers -32602 member_auth_binding_conflict, as does one whose binding cannot be read, with an advisory retryable: true; an agent’s mob tools answer a policy denial coded member_auth_binding_conflict or member_auth_binding_uncarried; and a member build that would run off the binding (a fork seated before the launch named it, say) or make provider calls off it (Meerkat’s fallback web search, where the provider’s own search does not serve it, on a member rebuilt on a session that recorded it, say) fails with member_auth_binding_uncarried, also after an SDK session builder’s answer is applied, so a restored member reads broken with the reason. Such a build refusal reaches the caller as the error of the method that built the member, its message containing member_auth_binding_uncarried: and the reason; meta/spawn_member (-32602) also carries error.data {kind, reason} when the refusal reaches it as Meerkat’s typed session error. The data shapes are in Native realm and member auth binding. A wake that Core holds until an operator acts answers -32603 with a structured kind (IdentityWakeHeldError in both SDKs): mob_member_session_repair_required (the session fails meerkat’s audited-endpoint guard), identity_role_migration_refused (the role change needs a declared migration) or mob_member_session_resume_held (another durable hold Core names in durable_resume_hold or resume_unavailable_reason). The identity’s wake_refusal names the way out. The lease codes -32003 (no active lease) and -32005 (lease lost) are no longer answered; -32005 is never reused. An identity whose held member a held pass did not publish (a held roster bootstrap or a held member add that aborted, or whose activation expired) owes its host a held completion. Every wake, reset or delivery outside a held pass that covers it answers -32603 with structured data {kind: "held_completion_owed", identity}, and a refused meta/reconcile_identity outcome carries the same error.data. The identity waits for a held pass to publish it; it has not failed. Both SDKs surface the data on a plain RpcError (see Adding one held member to a running roster). An identity whose member id is held by a member the host made directly on the mob (a live or retiring member whose spawn labels carry no agent_identity naming the identity) is not taken over. Its wake answers -32603 with structured data {kind: "identity_name_held_by_raw_member", identity, member, session_id}, and a roster compile that declares it fails with the same error; nothing is registered, woken, respawned or retired. Label that member for the identity on its session, or retire it, then try again (see Upgrading to 0.9.4). The -32010 constant is exported from both SDKs as MOB_EVENTS_STALE_CURSOR_CODE. The Python and TypeScript SDKs auto- reify -32010 errors into MobEventsStaleError (a typed subclass of RpcError) carrying after_cursor and latest_cursor. The numeric code alone is not enough on HTTP: -32010 with data.kind = "read_only" is a console policy refusal, not a stale cursor. Interpret the method and structured data before applying cursor recovery. Read-only errors apply to classified mutations; ABAC may deny an applicable action gate with -32030, or filter a read’s results rather than reject it. Console timeline replay failures use CONSOLE_TIMELINE_REPLAY_UNAVAILABLE_CODE so they are not mistaken for mob-events ledger cursor errors. The -32019 constant is exported as COMPOSITION_PROVENANCE_CODE and reified as CompositionProvenanceError, which exposes the refusal’s kind and fields (for example the profiles.lead.tools.deny path a candidate launch diverged on), so a deliberate composition refusal is reported without parsing the message. Before this code existed these refusals were -32603. The -32014 constant is exported as STORAGE_RESOLUTION_CODE and reified as StorageResolutionError, so a deliberate durability refusal at startup is a typed init failure, not a transport error. Storage refusals raised by the Rust builder surfaces (UnifiedRuntime::bootstrap) still surface as -32603 with the typed message text. The meta/init runtime_options storage declarations (runtime_store, event_log) are documented in Configuration → SDK and Gateway runtime options.

Topology control

Topology mutation is an optional Meta control plane. It is disabled by default; a host must explicitly configure TopologyControlPolicy as read_only or editable. The stock console only shows its Connections view when the runtime advertises topology management, and it never invents a connect all operation. Every mutation is evaluated against both endpoint identities. With access control enabled, callers need agent.view plus topology.view to inspect an endpoint, and the action-specific topology.connect, topology.disconnect, or topology.reconnect grant on both endpoints. Planning requires the same action-specific endpoint grants as applying; it is side-effect free, not an authorization oracle. Durable mutation history and actor/principal attribution require the separate topology.audit grant on every endpoint in the returned record. Requests containing multiple explicit operations additionally require topology.bulk and remain bounded by the policy’s finite max_batch_size.

meta/topology/query

Return the logical roster, observed/declared/operator topology, durable suppression tombstones, current revision, and caller-specific per-endpoint affordances. A disconnected declared edge remains in the response with suppressed: true; this is how operator intent survives reconciliation and restart.

meta/topology/plan

Validate a side-effect-free, revision-pinned set of explicit operations.

meta/topology/apply

Apply a previously valid shape of request. expected_revision provides compare-and-swap protection and idempotency_key is mandatory. Keys are scoped to the caller: a Console caller (its verified human; else its authenticated principal, with the provider and issuer that verified it; else the open Console’s anonymous account) and the gateway’s JSON-RPC surface each have their own keys, so the same key from two callers names two operations and a retry replays only its own caller’s apply. An open Console without an access controller never resolves tokens, so there every caller, even one presenting a valid token, is that shared anonymous account; isolating callers needs require_app_auth or an access controller. Exact replay is durable only across the configured retention horizon: receipt_limit keeps full operation receipts, then idempotency_history_limit keeps bounded key fingerprints that reject ambiguous reuse after a receipt ages out. Replaying a retained key whose receipt is unavailable returns -32009 with data.kind = "topology_idempotency_receipt_expired"; replaying a key whose fingerprint is still in the compacted ring returns -32009 with data.kind = "topology_idempotency_history_compacted". Once both bounded horizons have expired, that key is contractually new and normal revision and policy validation applies—clients must not assume keys are reserved forever. connect, disconnect, and reconnect are distinct policy actions: reconnect removes a suppression and repairs an absent or one-sided desired edge, while disconnect records a tombstone before removing physical wiring.

meta/topology/operation/get

Look up a durable operation receipt by operation_id. The endpoint is useful after a client reconnect or an ambiguous transport failure; clients should not infer success from a dropped HTTP response. The actor field is omitted unless the caller has topology.audit on every endpoint in the receipt. This lookup is also bounded by receipt_limit; persist the original request and idempotency key so an ambiguous response can be replayed while its exact receipt remains retained.

meta/topology/audit/query

Read the durable, versioned mutation-attempt ledger. This includes denied, invalid, interrupted, recovered, applied, and rolled-back attempts, so it is more sensitive than graph inspection and is advertised only to callers with a topology.audit grant. Returned records additionally require agent.view, topology.view, and topology.audit on every endpoint in that record. Persist next_after_seq after every response, including an empty one. If a non-zero cursor is older than oldest_available_seq - 1, Meta returns -32009 with data.kind = "topology_audit_cursor_expired", after_seq, and oldest_available_seq; restart from after_seq: 0 only when replaying from the retained frontier is acceptable. A cursor equal to oldest_available_seq - 1 remains valid. ABAC filtering advances the cursor across records the caller cannot see. This can leave gaps in visible seq values and reveals only that retained topology activity existed at those sequence positions—not its endpoints, actor, principal, operations, or result. That aggregate activity-volume metadata is an intentional trade-off for a stable, non-duplicating forward cursor. Cross-authority JSON-RPC mutation currently fails closed. Same-process hosts that control both runtimes can use Meta’s bilateral host API, which checks both runtime policies and endpoint grants before changing either side.

Mob events

meta/mob_events/query

Scan the meerkat structural-event ledger and return matches.
The response always carries a numeric next_after_seq — even when the filter matches nothing — so polling SDKs keep a valid resume anchor (caller’s after_seq or the current latest_cursor).

meta/mob_events/subscribe

JSON-RPC handshake that returns a snapshot plus a subscribe_url pointing to the /meta/mob_events/stream SSE route. The URL carries after_seq (next_after_seq ∨ caller’s after_seq ∨ latest_cursor) and the original filters so the SSE handler picks up gaplessly with the same predicate.

Roster and reconcile methods

The SDK stdio surface serves the roster family. POST /console/rpc has separate implementations of member reads/lifecycle operations and reconcile_edges, subject to the attached runtime’s capabilities. rediscover, reconcile_identity, and module reconcile are SDK/stdin-only. The covered unified/stdin read arms use the -32017 budget; not every roster operation is wrapped. The stock HTTP dispatcher does not apply that wrapper, so host/client HTTP timeouts are a separate concern. See Roster and member lifecycle for the lifecycle model and Profiles are templates for why none of these methods spawn members from [profiles.*] on their own. On rpc_gateway the roster itself comes from the SDK host. The gateway sends callback/roster_provider/roster with params: {"context": {"mob_definition": ..., "previous_identities": [...]}} and expects a DurableAgentSpec[] reply. The SDK dispatchers decode params.context into typed RosterContext fields (mob_definition and previous_identities in Python; mobDefinition and previousIdentities in TypeScript). The definition is optional for custom hosts. previous_identities is call-site-dependent, not an authoritative membership snapshot on every callback: topology-query and edge-reconciliation snapshots supply registered identities, while 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. meta/spawn_helper and meta/fork_helper reply only after the helper’s turn has finished and its retirement has been attempted, and the gateway applies no deadline to that, so a client should give these requests a reply timeout that covers the whole helper run. A fork helper is retired when its one turn ends, so it cannot be continued; a conversation a human continues is a side chat (meta/fork_member, below). On the Console, meta/fork_helper answers a source the console’s visibility policy hides as unknown (-32001, data.kind identity_hidden_by_policy) before its conversation is read, as meta/fork_member does.

meta/fork_member (Console)

Forks an agent’s conversation into a side chat: a durable fork of the agent’s committed transcript into a new member that a human continues in, beside the agent. Served on POST /console/rpc only, and listed in the Console’s meta/capabilities where the gateway’s sessions persist (a durable fork needs them) or its composition did not record whether they do (the fork then refuses typed where they do not). The agent’s session, transcript and queued work are not touched.
The fork takes the agent’s transcript as it is now; a message_count prefix is refused with -32602. It needs agent.spawn for agent_identity, and agent.view and agent.send for the agent: a side chat runs with the agent’s tools, so a caller who may only read the agent cannot fork it. A refused check answers -32030 naming the action, as does an agent whose lineage already fills the access rules’ walk (the agent and seven ancestors; reason: "lineage_too_deep"), since no one but an admin could open its side chat. That bound is one for every caller, an admin included. An agent the console’s visibility policy hides is answered as unknown (-32001, identity_hidden_by_policy), as meta/fork_helper answers it. The fork is refused on a read-only console (-32010) and on a candidate launch (-32047). An agent seated and never woken has no conversation to fork and is refused (-32055, cause no_session) without being woken; a dormant agent that has run forks from its stored session and stays dormant. The fork is bound to the agent session its checks read: if the agent moves to another session before the fork (a reset or a respawn ran in between), the side chat is retired again and the fork answers -32055 fork_source_changed. The side chat:
  • takes the agent’s role, profile and model overrides and runtime mode, and through the fork its application labels, context and tool overlay: it runs with the agent’s tools;
  • has exactly the agent’s audience: every agent check on a side chat is the agent’s (see Side chats). A rule that matches the agent, by identity, role or labels, or the agent’s own lineage, covers its side chats for every action (agent.view, agent.send, agent.retire, …), a deny on any of them hides them, and a rule that matches only the side chat (its name, its role or a label only it carries) grants nothing;
  • is never wired to a peer, whatever the agent’s role: the runtime seats it exempt from the mob definition’s wiring rules (auto_wire_orchestrator, role_wiring), so it gets no edge they declare and no member seated, spawned or rotated after it is wired to it; the definition-wiring reconcile and the topology’s declared edges leave out every member exempt from those rules, side chats among them, and a host’s own edge discovery policy (EdgeDiscovery) is never handed one;
  • is kept until it is retired (meta/retire or meta/retire_member, which need agent.retire on the agent): the fork sets an explicit keep-until-retired idle-retire override, so a side chat never idle-retires. It survives a gateway restart; the runtime may unload it while it is idle. meta/reset_all retires it with the other members that have no registered identity;
  • is never respawned or reset: meta/respawn, meta/respawn_member and meta/reset refuse it, on the Console and on the stdio gateway, with -32602, data.kind side_chat_restart_refused (operation, agent_identity, source_member_id), since its successor would be no fork of the agent. An agent’s mob_respawn tool answers a policy denial with that kind, and meta/reset_all, where a host declares a baseline member under the side chat’s name (without identity-first authority), lists it under failed with kind side_chat_restart_refused and leaves it as it is. Close it and fork the agent again;
  • is never adopted as an identity: mob/adopt_member_identity_declaration refuses it, on the Console and on the stdio gateway, with -32602, data.kind side_chat_adoption_refused (agent_identity, source_member_id), before anything is written: an adopted identity is rebuilt from its declaration, which no fork made;
  • names the agent in its console labels, side_chat_of (the agent’s identity) and side_chat_of_session (the session it was forked from), and in its experience sidebar row’s side_chat_of. They are read from Meerkat’s durable roster (the member’s fork source), on the experience, on the list calls (meta/list_members, meta/find_members, meta/console/list_identities, GET /console/identities) and on event streams; either label supplied any other way, in spawn or roster labels or an identity’s declaration, is dropped. A multi-runtime Console names the agent with its runtime’s prefix, as it names the side chat;
  • does not take the agent’s own console flags: where the agent carries them, addressable, singleton, console_watched, console_degraded, console_degraded_reason, console_alert_level, console_inbox_count and implicit_delegate_idle_retire_secs get side chat values.
Instructions: the agent’s activated instructions arrive in the side chat as history only. For each one the agent activated itself and had in effect at the fork, the fork activates the same revision and body in the side chat as the host. Any other stays inactive, and the side chat refuses its first turn naming it until a host activates it (meta/member_activate_instruction). The result lists each with state inherited or inactive and a reason (not_effective_in_source, body_unreadable, head_body_retired (the source’s own head of the key holds a retired body, which only a torn source document does at the tail) or activation_refused). Under the refuse fork instruction policy a fork of an activated agent answers Meerkat’s -32037 fork_instruction_refused. Result:
Errors: -32055 (typed refusal, see the error codes above), -32030 (agent.view or agent.send refused on the agent, agent.spawn refused on the side chat’s name, or the agent’s lineage fills the walk), -32001 (an agent the console’s visibility policy hides), -32037, -32602 (a missing or invalid param, an unknown agent, a prefix, or, under runtime_options.member_auth_binding, an agent whose session records another binding or none: member_auth_binding_uncarried, since the runtime seats a side chat from its agent’s records, nothing forked), -32004 and -32603. A fork that committed its session but could not seat the side chat keeps Meerkat’s fork_member_provision_failed data with the fork_session_id. Known limitations:
  • A side chat is in no declared roster. A host that reconciles the whole mob against a declared roster (UnifiedRuntime::reconcile on a runtime without identity-first authority) retires its side chats there, as it retires every member the roster does not name. The stock gateway is identity-first, does not reconcile that way, and keeps them.
  • A side chat forked before a launch named runtime_options.member_auth_binding is refused at its rebuild under that launch (member_auth_binding_uncarried), as is every fork seated before the launch named the binding: the runtime rebuilds it from its own records, which carry no binding or another one. Close it and fork again.

meta/ensure_member

Ensure a member exists, spawning it when missing. On the SDK-facing rpc_gateway this is the ephemeral worker plane (MobHandle::ensure_member); on an identity-first console gateway it upserts a durable identity unless the request carries plane: "worker" (see ensure_member gateway profiles).
After every successful ensure the gateway runs reconcile_edges, so declared definition wiring (auto_wire_orchestrator, role_wiring) converges regardless of the order members are brought up; calling reconcile_edges right after ensure_member is redundant. Worker-plane spawn refusals use -32602 on stdio and -32000 on HTTP, with the meerkat error in the message, for example ensure_member failed: wiring error: profile 'x' has tools.comms=false; mob meerkats require comms=true. Every mob profile must set tools.comms = true (the field defaults to false, so omitting it is refused too): a member’s identity, roster occupancy, wiring, and peer messaging are all keyed on its comms name. To keep a member from messaging peers, use read_only = true or a per-member tool policy, not comms = false. On the identity plane the same refusal does not error the RPC: the receipt’s outcome is broken and the reason lives in the identity’s bootstrap status error.

meta/reload_member

Non-destructive cold reload of an identity’s live member. The live runtime registration is discarded and the SAME durable session is re-materialized from durable truth: same identity alias, same session_id, same generation. This is the repair for a member whose sends fail with the reload-required class (meerkat’s RecoveryRepairBlocked: “registration- authorized cold reload is required”). The delivery path runs exactly one such reload automatically before failing typed; this verb is the operator’s manual form. Contrast meta/respawn_member, which on an identity-first gateway resets the identity (a fresh session at a new generation). reload_member never calls it. Result: Broken identities are refused (-32603, invalid state): meta/delete_identity starts them over. The one exception is an identity whose last wake Core held with a session_repair_required hold (below): for it, reload_member IS the repair path. After the operator has run the two rkat session repair-wholeblob commands the hold names, the same verb with no extra flag re-attempts the resume of the same durable session (same session_id, same generation), clears the hold and wires the identity’s managed edges again (Core held no member for it, so it held none of its edges); a document that is still refused holds the wake again, typed. Retired identities return not_current without being revived. The primitive’s not_current is also a no-op, not a fallback to retire/rematerialize. Worker-plane members are refused: the worker plane has no non-destructive reload. Two typed failures come from meerkat’s reload primitive: a refusal (“store not healthy, reload refused, retry later”: the durable resume authority is still unreadable, the registration is retained, and sends stay reload-required) and a timeout naming the stage it reached (MEMBER_RELOAD_TOTAL_TIMEOUT, 45 s; inspect the member before retrying). Both are visible afterwards in meta/member_health.last_reload. A third, non-retryable failure is meerkat’s typed audited-endpoint divergence (SessionError::WholeBlobAuditedEndpointDivergence, JSON-RPC -32013, resume hold audited_endpoint_divergence): the member’s committed WholeBlob document keeps every message, but its live transcript no longer preserves the graph-proved audited endpoint, so every read refuses and no reload, heal, or reconcile can change that. Meta reads the hold typed (never from error text), records it as the identity’s wake_refusal with a session_repair_required hold (an identity Core holds as a member keeps reading active; one it holds no member for reads dormant), and returns -32603 with error.data:
durable_resume_hold is the same key and token meerkat puts on its own refusal, so a caller classifies both the same way. No background pass retries a held identity (no heal call, no reconcile retry, no re-attempted resume), and meta/member_health.last_reload.outcome records the attempt as repair_required. Operator path: run session_repair_required.diagnose_command, then session_repair_required.apply_command (add --accept-shorter only when the diagnose report says the live transcript is shorter than the audited endpoint), then meta/reload_member on the identity.

meta/member_health

The identity’s lifecycle and delivery health from in-process reads only. It never crosses the mob actor’s command loop, so unlike meta/member_status it answers while the loop is stalled, which is when an operator needs it. Result (MemberHealthReport): Actor timeouts preserve {kind: "mob_actor_command_timed_out", command_kind, stage, deadline_reached: true} in RPC error.data, console error data, and last_delivery_error.data. The upstream stages include actor_command_admission and actor_command_reply; the latter may follow runtime admission. A local observation timeout has kind actor_admission_timeout and no invented upstream command/stage. Neither form asserts execution fate or retryability: absent executed or retryable fields are not a nonexecution verdict. Consult runtime idempotency and durable fate; Meta does not automatically retry or reload on a timeout. The same observations survive an explicit or automatic reload. In-flight probe interruptions carry observation: "in_flight", distinct from a true "before_call" refusal, and do not settle the reload worker’s execution fate. Custom SessionBridge::member_durability implementations may await their own I/O, but do not hold the runtime’s fleet entries lock. Admission backpressure and reload failures retain sanitized owner data across send/dispatch errors, console REST/RPC, and health: mob_member_admission_backlog_full carries depth, mob_member_reload_refused carries its kind only, and mob_member_reload_timed_out carries the original stage. These payloads add no execution or retry verdict and omit backend refusal details.

meta/reconcile_edges

Converge the definition-derived edge policy over the live roster and return {desired_edges, wired_edges, unwired_edges, retained_edges, preexisting_edges, skipped_missing_members, pruned_stale_managed_edges, failures}. The definition-policy reconciler only unwires edges it wired itself. In an identity-first runtime with a host TopologyProvider, the provider owns identity edges: an identity edge that neither the provider nor the definition’s wiring declares is unwired, including an edge wired before a restart (see Roster). It is needed after meta/reconcile_identity or an identity-first gateway boot when the definition declares wiring, because neither of those converges definition edges after materialization; without declared wiring the report is empty. On a candidate launch (runtime_options.mob_composition.authority = "candidate") the reconcile is a dry run: it wires, unwires and prunes nothing and writes no topology journal. The report has the same fields, naming what the authoritative launch’s reconcile would wire (wired_edges), unwire (unwired_edges) and prune (pruned_stale_managed_edges), plus "applied": false, and the gateway logs a candidate launch peer-edge census line. A report without applied was applied.

meta/reconcile_identity

Re-run the roster provider and restore_flow (Python runtime.reconcile()). Picks up added, removed, and modified identities and host TopologyProvider edges. It does not run reconcile_edges. Unavailable without an identity-first runtime.

meta/rediscover

Reset the whole mob, re-run the Rust builder Discovery, respawn discovered members, and reconcile edges. Neither gateway binary wires a Discovery, so on every SDK host this returns {status: "no_discovery_configured"}; with identity-first authority attached it is refused outright (-32000, rediscover resets the whole mob and is unavailable with identity-first authority; use refresh_desired_topology).

Identity lifecycle methods

Identity-first runtimes only. Core’s mob machine holds every identity’s session and generation, and each answer reads them at request time. An identity’s state is active (Core holds a member for it: every roster identity is seated in Core’s mob, bound to its session, and is materialized when work reaches it, so active does not say whether it is materialized in this process now), dormant (Core holds no member for it: the roster no longer lists it and Core retired it, or it is not seated yet) or broken: Core holds no member and the last attempt was refused for good, or Core holds the identity’s member Broken because it could not restore it (for example its comms identity is missing on this host, or its session cannot be resumed; agent_runtime_id is then set and there is no wake_refusal). Callers must tolerate future values. A read never materializes a member. A delivery to a member that is not materialized (a host send or dispatch, a peer message, a schedule fire, an announcement) materializes it first, through Core, and is refused typed when Core cannot: -32052 for a refusal a retry can clear, as for a wake that ran past its bound or a stopped mob (whose members, live ones included, take no delivery until the mob runs again), -32603 for Core’s final refusal (a retiring incarnation, a host-owned or candidate member), and -32014 while the identity’s first-boot row is pending or refused, or the first-boot manifest cannot be read. wake_refusal is {code, session_id, remedy}: code is -32051 for a final refusal or -32603 for a held one, and remedy is delete_identity, repair_session, resolve_role_migration or operator_review. A refusal does not change the state of an identity Core holds as a member: it reads active (or broken while Core holds its member Broken), with the refusal on wake_refusal. An identity Core holds no member for reads broken after a final refusal and dormant after a held one. replaced_session was set when Meta woke an identity on a fresh session because Core held no copy of its last one; Meta no longer does (Core decides an absent session: it repoints the member to a successor, or holds it Broken), so it is absent, and kept for wire compatibility. A status read during a respawn or reload can report dormant with the previous session and generation for a moment; the next read reports active at the new generation. The verbs refuse typed (see Meta-specific error codes): -32001 for an unknown identity, -32051, -32052 and -32053, -32603 with a structured kind for a held wake, and -32014 (first_boot_row_pending, retryable) for a reset or delete while the identity’s first-boot manifest row is pending.

meta/delete_identity

Start the identity over: its earlier session is no longer resumed, and its next session starts empty. The result is {identity, cleanup_warning}.
  • The identity moves to a fresh session in one Core rotation: Core retires the current incarnation and seats the identity on the fresh empty session in the same step, so it is never without a member. Meta materializes nothing: the fresh session starts on the identity’s next delivery or, when its spec owes a kickoff, on that kickoff, which Core runs exactly once on the new session. A rotation a crash interrupts is recorded first, and the next start drives it to its end. An identity the roster no longer lists is never made live by the delete.
  • An identity Core holds no member for (its last member was retired) is seated on the fresh empty session by the same rotation, as one fresh seat.
  • Core’s rotation ends the identity’s wiring with its incarnation: Meta wires its managed edges (the roster’s and the topology controller’s) again at once, bounded, and a re-wire that fails is logged and retried by the next reconcile. A cross-mob edge (wire_cross_mob_by_identity) ends with the incarnation it named: wire it again for the new one.
  • A delete Core refuses before any of it starts (the mob is not running, or another lifecycle operation of the member is in progress) answers -32052 (retryable): nothing was retired, and no later start carries it out. So does a delete whose step cannot read the head of Core’s member lifecycle journal or record the side effects it owes: nothing was asked of Core.
  • A delete whose rotation failed after any of it reached Core (or where Core’s journal cannot tell) answers an internal error (-32603) saying it may have started: the identity’s next roster compile drives it to its end, so read meta/status_identity and do not repeat the delete.
  • An identity Core never spawned has no history: the delete succeeds and changes nothing.
  • An identity a host adopted is refused with -32053, and an unreadable history with -32052 (retryable).
  • If Core committed the fresh session but its binding cannot be read back, the delete answers an internal error (-32603) saying the fresh life is committed: read meta/status_identity, and do not repeat the delete.
  • The delete clears a broken state and wake_refusal.
  • The old session is not deleted. Core keeps it archived with its transcript: session listings hide it, and a history read by its session id still serves it, also after a restart. The result does not name it, so read meta/status_identity first to keep its id.
  • Meta distills the identity’s agent memory (bounded) before the fresh session starts, and drops the old session’s memory scope only after Core has committed the fresh session, so a refused or failed delete keeps it. The identity-keyed memory store is kept, and the old session’s subscriptions end.
Meta 0.9.3 deleted its session snapshot and the identity’s continuity record instead, and left the identity dormant until the next reconcile; Core’s runtime copy of the old session stayed on disk, unbound.

Console timeline

Console-specific RPC methods are handled by the HTTP console dispatcher and are also listed in the console contract v0.5.0. Surface split: meta/console/* methods are served ONLY by the HTTP console dispatcher (POST /console/rpc). The stdio JSON-RPC surface (rpc_gateway stdin) neither dispatches nor advertises them — embedders needing console data over stdio should use the http_base_url from the init handshake and call the HTTP surface. The advertised-methods list on each surface reflects exactly what that surface dispatches.

meta/console/list_identities

List identities known to the console aggregator. An identity-first record carries member_id, Core’s mob member id of the identity’s last binding, as on meta/status_identity; it is absent when unknown and on a raw runtime-alias record.

meta/console/inspect_identity

Return {identity: ConsoleIdentityRecord, peers: string[]} for one console identity. The record includes health and addressability; affordances are projected by /console/experience, not this response. An identity whose last wake Core held for the audited-endpoint repair (a session_repair_required hold) reports health: "needs_repair", and the console status and inspect payloads carry the hold as session_repair (the same object as meta/member_health.session_repair_required), which the console renders as the needs repair state with the two repair commands. Older servers may only support the legacy meta/inspect_identity; the headless console controller keeps that as a compatibility fallback.

meta/console/query_timeline

Query the console log store for timeline frames. The method backs the bundled console chat and activity panes.
Response:
next_cursor never advances past visible frames omitted due to limit, so clients can page with mode: "since" without gaps. latest_cursor is the live-continuation cursor for mode: "recent" seed queries. Backfill frames are not fresh completions. Session-history backfill re-emits past turns as frames whose source.kind is "session_history". They carry session_id, and they carry the SAME interaction_id and run_id the live frames carried: meerkat persists the message identity onto committed transcript messages and the backfill stamps it back (null only for messages that predate that persistence). Identity queries and every session-bearing live frame can trigger backfill, and a history twin of a tool-only assistant step arrives as an interaction_complete with empty text, so matching by interaction_id alone does NOT separate live completions from history. Any consumer correlating completions MUST exclude source.kind == "session_history" frames from live turn handling, ignore frame_updated marker frames (emitted when a frame’s status changes, for example accepted to delivered), and then match interaction_complete on the interaction_id returned by meta/console/send.

meta/console/send

Submit an identity-addressed console interaction through the console aggregator.
origin_kind is optional. When present it must be one of operator, operator_probe, connector, scheduler, policy, flow or system (ConsoleTurnOrigin); other values are refused with invalid params. It is persisted on the user_input frame so the console transcript labels the turn from typed data. The response includes interaction_id and identity. The multipart JSON-RPC endpoint also accepts this method when content contains image_upload placeholders. origin and idempotency_key are required, non-empty strings after trimming. Deduplication is scoped by (caller, runtime, identity, origin, idempotency_key). The caller is the authenticated console principal, taken from the request’s auth and never from a parameter: a verified human by OIDC issuer and subject, otherwise the authenticated principal (a service identity, or a token without a subject) together with its provider and the issuer its token verified against. Anonymous callers of an open console share one local account. An open console without an access controller never resolves tokens, so there every caller, even one presenting a valid token, is that shared account; per-principal isolation needs require_app_auth or an access controller. The same key from two callers names two different sends: neither replays nor observes the other’s. Mint a new key for a new send; to replay an ambiguous send, retain the same scope, content, and effective handling_mode (omission means queue). A matching replay returns the earlier interaction’s acceptance whether or not the turn has finished. Changed content or mode (for example queue to steer) within that scope is refused with -32009 (idempotency_conflict, HTTP 409 on the REST form). Changing the target or origin can create a distinct send. Member respawn does not clear the key, but this does not promise replay after the deduplication records or runtime are lost. A send is accepted (its user_input frame) before it is submitted for native admission, and settled (delivered or delivery_failed) after. When the gateway stops in between, a replay of the same send decides from the runtime’s admission evidence in the session the send recorded. An admitted input settles the send delivered. For a send to a durable identity’s local session, an absent one is dispatched again under the send’s own delivery identity and pinned to that session, so the runtime keeps one input; if the session has moved, the send ends delivery_failed and is never retargeted. A governed send, or any other member’s, is settled from evidence only and never dispatched again. A replay waits for that evidence as long as meta/console/recover_send does (below). At startup the gateway settles, from evidence only, the unsettled sends among the newest console frames whose input was admitted.

meta/console/recover_send

Recover the caller’s own console send: its acceptance as it is now and what the runtime holds for it. Mutating (it can settle a send) and gated like meta/console/send (ABAC action agent.send on the identity).
The three fields are required and nothing else is accepted: interaction and frame ids are not lookup keys. The result is the send’s acceptance plus native:
native.state is admitted (with the input id and its lifecycle phase), absent (no input under the send’s delivery keys in its recorded session; a member reached through an external binding carries no delivery key, so for it this is not evidence that nothing was delivered), or unknown (with a cause). A send the evidence shows admitted but not yet settled is settled delivered; nothing is ever dispatched. A key the caller never used, and a key another caller used, both answer -32001 with data.kind = "send_not_found". The REST twin is POST /console/send/recover. Reading the evidence loads the session the send recorded from the runtime’s durable store, which after a restart can take seconds on a remote store. The call waits up to 45 seconds in all for the evidence of both delivery keys, then answers native.state unknown. A client whose own timeout is shorter gives up first: the console and the SDKs’ stdio transports wait 60 seconds by default, and console_fetch_timeout_ms, the bundled console’s fetch timeout, should stay above 45 seconds. skill_refs is optional: typed, source-pinned skills selected for this exact turn, in the same shape as meerkat turn/start:
The selection travels with the turn to the addressed member through the same fenced host-human admission as the content. The member’s own skill engine resolves it: the turn emits the typed skills_resolved or skill_resolution_failed event and commits a durable SkillContext block. Meta never renders skill bodies into the content. A member without a skill engine fails the turn with the typed no_skill_engine reason before any provider call. A nonempty selection is part of the replay scope: a retry must repeat the same selection in the same order to return the original acceptance, and a changed or reordered selection under the same key is -32009 (idempotency_conflict). Remote or externally bound members cannot carry a selection and refuse it typed instead of delivering the turn without it. Untyped skill strings are refused as invalid params.

meta/blob/upload

Upload one image through POST /console/rpc/multipart. Send a JSON-RPC payload part with params.upload (or the same fields directly in params) containing upload_id or its alias part_name, plus exactly one matching file:<id> part. Optional type is image_upload; optional media_type must match the file’s MIME type. The result is {blob_id, media_type, size} (size in bytes), with no promised url or content_type field. Use GET /blobs/{blob_id} to fetch the image. See the multipart example for the payload, file part, supported MIME types, and size limit.

meta/list_runs

List flow runs for this mob.
Returns {runs: MobRun[]} carrying the full meerkat ledger projection: step_ledger, failure_ledger, frames (map keyed by frame id), loops (map keyed by loop id), loop_iteration_ledger, flow_state, activation_params, schema_version, root_step_outputs, loop_iteration_outputs, and role_targets for a role-targeted run (see meta/run_flow).

meta/run_flow

Start a run of one of the mob’s flows.
Returns {run_id}, plus role_targets for a role-targeted run. Without role_targets, a step dispatches to every active member of its role. With them, each step dispatches only to the bound members of its role that are still active and still hold the role, never to another member of the role, so several runs can share one mob and one role. The binding must name every role the flow’s steps use and no other, no quorum step may need more members than its role binds, and each member must be an active member of the mob holding that role. Member ids are public aliases, and the encoded roster spelling is refused. A durable identity or plain member name names its own member. A generated runtime alias (rt:{identity}:{generation}) names its durable identity when the identity runtime confirms it is the current incarnation; a stale one is refused with -32000 and data kind stale_identity_runtime_binding, as every member action refuses it, and on a gateway without an identity runtime the alias keeps its exact spelling, which names no member (not_a_member). A role named twice in the JSON object keeps its last value, by JSON object rules. The binding is fixed for the run: when a step’s remaining bound members can no longer meet its collection policy, the run fails with insufficient_targets before that step dispatches anything, whatever the step’s failure_policy; no other member is substituted and no flow member is provisioned. An identity-first gateway still materializes its registered identities before admission, as for every run; that decides no membership. A malformed binding is refused as invalid params (-32602, no data). A binding the mob refuses is -32602 with data {"kind": "flow_role_targets_refused", "flow_id", "refusal": {"kind", ...}}, where the refusal kind is one of role_not_in_flow, role_unbound, quorum_exceeds_bound_set, not_a_member, role_mismatch or not_active (a closed set) beside the role, member and other fields it names; no run is recorded. On the console, a role-targeted run also needs agent.send on every bound member. The result echoes the admitted binding as role_targets, with members as public aliases. The run record (meta/flow_status, meta/list_runs) carries it as the runtime recorded it, in the mob roster spelling that every member id of the record uses, the root_step_outputs keys of a multi-member step included: a member id that does not start with a letter or _, or holds characters other than letters, digits, - and _, appears there in its encoded spelling. Since the encoded spelling never equals a public alias, a gate comparing aliases with the record can only withhold authorization; give members comms-safe ids (a letter or _ first, then letters, digits, - and _) so a gate can compare them directly. A gateway that serves role_targets lists flow_role_targets in meta/capabilities.feature_capabilities. An older gateway ignores the parameter and runs the flow role-wide, so a raw RPC caller must check the capability before sending role_targets, and must treat a result without the echoed binding as a role-wide run (cancel it). The Python and TypeScript SDKs do both: they refuse role_targets with CapabilityUnavailableError before calling, and cancel a run whose result does not echo the binding before raising FlowRoleTargetsNotAppliedError. A run record read through an older runtime has no role_targets; a check that requires a bound run must treat a missing binding as not authorized. With idempotency_key, the run id is derived from the mob, the flow and the key. A replay with the same key, params and role_targets answers with the same run, in flight or ended, without admitting it again, also when the first answer was lost. A run that a gateway restart cut off is canceled (execution_custody_lost), and the replay returns that canceled run: a new attempt takes a new key. The same key with other params or another binding is refused with -32602 and data {"kind": "flow_run_idempotency_conflict", "run_id", "flow_id"}, and no second run is admitted; params compare as JSON values, so send arrays in a stable order. A key is scoped to its flow: the same key on another flow admits that flow’s run. A malformed key is -32602 without data. A gateway that serves the key lists flow_run_idempotency_key in meta/capabilities.feature_capabilities; an older gateway ignores it, so a raw RPC caller checks the capability before sending one. The Python and TypeScript SDKs refuse a key with CapabilityUnavailableError before calling such a gateway, and raise FlowRunIdempotencyConflictError for a conflict.

meta/reset_all

Console surface only (POST /console/rpc; mutating; ABAC action agent.reset). The stdio surface neither dispatches nor advertises it, and the aggregator-only console (no runtime attached) refuses it with data.kind = "unsupported_reset_all_surface" rather than retiring anything. It resets every console-visible identity in one pass and never exits the gateway process: a slow reset_all is a long request, not a restart. The designed exits are elsewhere, and differ per binary: rpc_gateway ends when the SDK closes its stdin (EOF), on SIGINT or SIGTERM, or through the SDK’s shutdown handshake; meta_gateway keeps serving HTTP after its stdin closes and ends only on SIGINT or SIGTERM or when its HTTP server task ends. Both stdin behaviours are test-pinned (tests/gateway_concurrent_dispatch.rs). The pass is sequential, one identity at a time:
  1. Preflight, fail-closed. Every target is checked before anything is mutated: a live console alias that resolves to another identity’s runtime member, a registered identity whose live session disagrees with its binding (stale_live_identity_alias), or a registered identity when the identity runtime has no session bridge (identity_reset_requires_session_bridge). Any failure returns the body below with failed non-empty and nothing changed.
  2. Raw delegates, helper members with no registered identity, are retired.
  3. Each registered identity is reset through the identity runtime (one Core rotation onto a new generation and a new session; nothing is materialized, and the fresh life starts on the identity’s next delivery); a live member that carries no registered identity is respawned in place. Only raw delegates without a registered identity are retired.
  4. The call waits up to 10 seconds for the reset identities’ startup history to reach the console timeline, then returns.
Identities outside the caller’s console visibility policy are outside the target set and are neither reset nor retired.

Gating methods

meta/gating/evaluate

Apply gating policy to a proposed action and its caller-supplied or gateway-policy risk tier. This operational method is served over SDK/stdin, not POST /console/rpc.
action_id is generated by the server. outcome is allowed, allowed_with_audit, pending_approval, or safe_draft; pending_id and fallback_reason are nullable.

meta/gating/decide

Approve, reject, or escalate a pending gated action.
HTTP derives the approver from the resolved principal, ignoring any supplied approver_id in that case. This also applies to a valid voluntarily supplied token on an auth-optional console. HTTP additionally accepts "deny" as an alias for "reject"; stdio does not.

meta/gating/pending

List all pending gated actions awaiting approval.

meta/gating/audit

Return the gating audit log (most recent entries, bounded by retention limit).

Memory methods

meta/memory/index

Record an operational memory assertion or conflict signal. This surface is the Meta assertion ledger used by routing/gating flows; it is not semantic/vector document search. The runtime owns the ledger: it loads the local JSON file once at bootstrap, holds the assertions in memory, and rewrites the whole file (optionally health-gated by an external endpoint) on every index. The file is not a shared append log; edits made to it directly are overwritten, and the ledger keeps at most 4,096 assertions. See the ownership model. The stock RPC contract stores and filters canonical assertions.

meta/memory/query

Query the loaded in-memory assertion/conflict ledger using exact filters and an optional text filter. This does not probe persistence-backend health and does not return -32012 for backend unavailability.
This is ledger text filtering, not vector or semantic search.

meta/memory/stores

Return information about configured memory backends. Agent-memory methods are available over the SDK JSON-RPC gateway and over console POST /console/rpc when the identity runtime has an agent-memory provider configured. With ABAC enforced, console recall and manifest require both agent.view and agent.memory.read on the target identity; remember and forget also require mutating console access plus agent.memory.write and agent.memory.delete respectively. Older memory-naive policies receive compatibility normalization; explicit memory policies are applied literally.

meta/agent_memory/remember

Write an identity-scoped agent memory record for later per-turn and build-context injection. This is separate from meta/memory/*, which is the operational assertion ledger. A successful write means the configured hot identity-memory provider accepted the record; optional external memory hub enrichment or extraction, when supplied by a custom provider, is out of band.

meta/agent_memory/recall

Read identity-scoped agent memory records from the configured agent memory provider. This exposes the same durable record set used for per-turn and build-context injection. Unlike automatic injection, explicit recall reports provider errors to the caller.

meta/agent_memory/forget

Delete one identity-scoped agent memory record from the configured provider. Providers that do not support deletes do not advertise this method in meta/capabilities. forget prevents future provider recall and future automatic injection; it cannot remove text that has already been delivered into an active model context.

Routing methods

meta/routing/resolve

Resolve a logical destination to a physical route.
route_id is server-issued. Pass the complete returned resolution unchanged to meta/delivery/send; do not construct it from a member/channel name or confuse it with an authored route’s route_key.

meta/routing/routes/list

List all registered routes.

meta/routing/routes/add

Register a new route. Fields may be directly in params or wrapped as params.route.

meta/routing/routes/delete

Remove a registered route. Required params: {"route_key": "your-authored-route-key"} (non-empty string).

meta/identity/resolved_tools

The tools an identity’s member offers its model, without waking it.
The result is {identity, session_id, tools, tools_source}, and for a recorded answer also record_revision and published_at_ms: The vocabulary may grow: treat an unknown value like no_build_recorded read as “not a live list”, and keep it. Every build of the member writes its record, so after the member’s first delivery the answer is the full list, also across restarts. meta/identity/warm builds a member and writes its record without a turn. An answer from a gateway that predates the field has no tools_source, and is a live read.

meta/identity/warm

Materialize an identity’s member without a turn, and record its tools.
Core builds the member on its session, through the same wake a delivery takes, but no input is delivered and nothing is added to its transcript. Once the member is live, its live tools are written to its tool record, and the result is that record: {identity, session_id, tools, record_revision, published_at_ms}. A later meta/identity/resolved_tools answers them without a wake, also after a restart. A member that is already live is not built again; its record is written again from its live session. After the member is live, only its own managed edges are reconciled, bounded. A member that cannot be materialized is refused typed, as a delivery is: -32052 for a refusal a retry can clear (a stopped mob, a wake that ran past its bound, a host build still pending, Core’s history unreadable), -32603 for Core’s final refusal of the wake (a retiring incarnation, a host-owned or candidate member) and for a held wake (with its data.kind), -32014 while the identity’s first-boot row is pending or refused, -32051 when the warm must seat the identity first and Core cannot resume its session, -32001 for an unknown identity, -32053 for a host-adopted one. A candidate launch refuses the method. The Console serves it on POST /console/rpc with the same result and codes, for a caller its visibility policy shows the identity to, under the agent.respawn action.

meta/identity/routing_status

Meerkat’s typed model-routing status for the live session behind a Meta identity. The result is meerkat’s SessionModelRoutingStatus verbatim (WireSessionModelRoutingStatus is a type alias to it), flattened under identity and session_id. Meta declares no mirror type, so this payload cannot drift from meerkat’s contract.
Result fields: identity, session_id, baseline_model, effective_model, and the optional session_provider, active_turn_override, active_operation_override, pending_switch_turn. session_provider is the typed provider of the session’s current LLM identity. An absent session_provider means the runtime machine has no hydrated session LLM identity yet (pre-hydration). It is not a signal to re-derive a provider from effective_model: meerkat documents that re-derivation as silently wrong for models owned by a custom ModelRegistry, which is why the field is carried typed rather than inferred. This method requires a resolved session. Identity-first materializes an identity without activating it, so an identity that has been materialized but never addressed has no session and therefore no routing status. That is the expected state after a restart, not a defect. A fleet sweep should address an identity before reading, and label its coverage post-address rather than post-restart. Failures carry a machine-readable discriminator so a sweep can classify an identity rather than only fail it: Each reason is derived only from a fact Meta observed. session_not_held is matched on RuntimeDriverError::NotFound structurally, never on message text; because that error is #[non_exhaustive], every other upstream failure lands in upstream_read_failed rather than being asserted as a missing session.
SDKs: handle.identity_routing_status(identity) (Python), handle.identityRoutingStatus(identity) (TypeScript).

Delivery methods

meta/delivery/send

Send a message through a resolved route.

meta/delivery/history

Return {deliveries: DeliveryRecord[]} as a bounded history snapshot, not a cursor-paginated result. HTTP accepts limit (default 50); SDK/stdin also accepts exact recipient and sink filters and defaults limit to 20. Neither surface supports cursor pagination.

Registered rows and row-checked sends

meta/registered_spec ({"identity": "<id>"}) returns the identity’s REGISTERED roster row: {"identity", "spec", "spec_digest"}, where spec is the full row the runtime holds and delivers under (including additional_instructions) and spec_digest is its canonical digest (sha256:<hex> over the row’s JSON with sorted keys). It is not the roster provider’s desired row and not proof that the member’s actor adopted every field. The read is informational. A host that authored an input from that row passes the digest back on meta/send as "expected_spec_digest". The runtime compares it with the registered row in two separate acquisitions of the identity’s lifecycle lock: for a non-Active identity in the embodiment decision, and again before memory and delivery. A row that is stale at the embodiment decision does not wake the identity; a row that was current there can still wake it, and a change committing before the final check then refuses the submission. Either refusal is code -32006 with data {"kind": "registered_spec_changed", "identity", "expected_spec_digest", "submitted": false}: nothing is submitted, but the refusal alone does not prove that no earlier wake happened. Every row writer (roster registration, reset adoption, update_spec) takes the same lock, so a row change that commits after the check waits behind the delivery: a queued change alone never refuses a send.

Identity delivery and turn tickets

Identity-addressed methods (these, the identity status, inspect and lifecycle methods, and the operator member methods meta/member_activate_instruction, meta/member_instruction_activations, meta/compact_member and meta/bound_member_transcript) take the durable identity or its current generated runtime id, rt:<identity>:<generation>. A runtime id that is no longer the identity’s binding, such as the predecessor’s after a reset, is refused with -32000 and error.data.kind stale_identity_runtime_binding, naming the current binding in registered_runtime_member_id and registered_session_id; the member methods and the console’s identity routes, meta/console/inspect_identity included, answer the same. A runtime id of an identity the runtime does not know answers -32602 invalid identity: runtime identity not found: <id>, and a stale runtime id of an identity this surface hides (an implicit delegate member) answers -32602 invalid identity: identity hidden by policy: <id> and names no binding. A runtime id whose identity’s binding cannot be read right now is never passed as the current binding, nor refused as a stale or unknown id. The member methods and the role targets of meta/run_flow refuse it with -32052 (identity_history_unavailable, retryable), and so do these methods once their target lookup has read Core’s member history; when that lookup’s own read fails they answer -32602 invalid identity: identity <id> was not resolved: <detail>; nothing was changed, as for any target. The console’s routes answer -32052 from their identity lookup and their runtime id checks, and can answer -32000 with the same error.data.kind when a route’s own status read fails after its lookup, or when the id names a live member whose binding cannot be read. meta/send, meta/interact and meta/dispatch deliver to an identity and return completion_baseline, the identity’s completion cursor read before delivery. That cursor is an identity-wide barrier: any completion on the identity after it (a peer message, a scheduled turn, a fork completion wake) passes it, so it does not name one turn. Pass "track_turn": true to name the admitted turn with a ticket. The result then also carries "turn": {"ticket": "<uuid>"}. The runtime mints the ticket for that one admission and keeps the admitted turn’s own completion handle under it. The caller’s interaction id (meta/interact) and a dispatch’s correlation id still ride into the runtime unchanged, but neither names the ticket: both may repeat across admissions (one correlation fanned out to several identities, a reused interaction id), and a ticket derived from them could name two turns. An idempotent re-dispatch (meta/dispatch with the same idempotency_key to the same identity) whose tracked original, on the identity’s current session, is still pending or completed is deduplicated by the runtime onto the original, and returns the original’s ticket. The runtime deduplicates only within one session’s ledger, so after a session rotation the re-dispatch is a turn of its own. Any other re-dispatch the runtime deduplicates (its original was untracked, failed, or aged out) is tracked on its own and settles with what the runtime reports for the deduplicated input: the original input’s result while that input is still running (the runtime shares its completion), and output_status: "no_own_result" once it has ended. When the turn cannot be tracked, the result carries "turn": null plus "turn_unavailable": {"code": "<code>", "reason": "<text>", "delivered": <bool>}. delivered is false only for not_delivered; every other code means the delivery happened, exactly once, on the ordinary lane: meta/interact refuses a caller-supplied interaction_id that is still in flight on the identity (queued, the active interaction, or the interaction of the run in progress) with -32602 and error.data {"code": "interaction_id_in_flight", "identity", "interaction_id"}, before anything is delivered: two in-flight runs under one transcript interaction id would make both runs’ frames and terminals ambiguous. An id whose interaction has settled may be sent again, but the transcript then holds two interactions with the same id, so mint a fresh UUID per send.

Waiting for completion

Completion waits are server-side and event-driven. The gateway parks the request and answers when the awaited change happens: it waits on the runtime’s typed completion and ticket-settlement signals, so neither the client nor the gateway polls. The caller’s timeout_ms is only a deadline; reaching it is a typed answer, not an error. The recommended completion barrier is "track_turn": true on the delivery plus meta/wait_for_turn on the returned ticket (the SDKs’ send_and_wait / sendAndWait, or wait_for_turn / waitForTurn). It reads only the ticket ledger, names exactly one admitted turn, and returns that turn’s own output. An identity-wide wait on completion_baseline uses meta/wait_for_completion and reads the output once, afterwards. Do not poll meta/inspect_identity for completion: it reads the member session (an execution snapshot on its session task), and a tight poll of it can hold a staged run from starting on large sessions. The SDKs’ wait_for_completion / waitForCompletion, wait_for_output(after=...) and wait_until_ready / waitUntilReady use the server-side waits, and fall back to polling only against a gateway that predates them.

meta/wait_for_completion

Wait until the identity’s completion cursor moves past after, or, with no after, until it counts at least one completed turn (a startup readiness barrier; the identity may not be materialized yet when the wait starts). Return {identity, outcome, completion_cursor}. It never touches the member session, and every wake re-reads the identity’s entry, so it never runs to its deadline on an identity that can no longer complete: A baseline for an identity that is not registered is the typed -32001 unknown-identity error. Callers must tolerate future outcome values.

meta/completion_cursor

Return {identity, state, completion_cursor}: the identity’s lifecycle state and its completion cursor {epoch, turns}, read from memory. A one-shot read that never touches the member session; wait with meta/wait_for_completion, not by polling this. completion_cursor and state are null for a raw live alias that no identity authority tracks (not “no turns yet”). An unknown identity is the typed -32001 unknown-identity error.

meta/wait_for_turn

The meta/turn_result report, taken once the ticket settles: the gateway waits on the ticket registry’s settlement signal. The result adds wait: settled (the turn left pending, or the ticket is unknown), timed_out (the deadline passed with the turn still pending) or shutting_down (the gateway is shutting down; the turn’s state at that moment). An unknown ticket answers unknown at once. Parameters are those of meta/turn_result plus the optional timeout_ms (default 60 000, clamped to 600 000).

meta/turn_result

Report one ticketed turn: {identity, ticket, state, completion_cursor, ...}. state is pending, completed, failed or unknown.
  • completed adds output_status, output and output_truncated. output_status is text (output is the turn’s own final assistant text from its committed run result, cut at 256 KiB when output_truncated), empty (the turn committed no text), no_own_result (the runtime folded the input into a run already in progress, or deduplicated it onto an earlier admission whose turn had ended; no output belongs to this admission) or unavailable (the gateway’s session bridge cannot report per-turn output). output is null for every status but text.
  • failed adds error. A turn that ran and succeeded is never reported failed because its session could not be resolved afterwards; that failure is recorded on the identity’s last_delivery_error instead.
  • unknown means the gateway knows no such turn for this identity: never admitted there, another identity’s ticket, aged out (settled turns are kept for 10 minutes, at most 4096 tracked turns, the turn settled longest ago going first), or the gateway restarted since. It is never a guess in either direction.
The SDKs’ send_and_wait / sendAndWait and dispatch*_and_wait send with track_turn and wait with meta/wait_for_turn (polling meta/turn_result only against a gateway that predates it). They warn, typed, when they return truncated text or a turn completed without output of its own. When a delivery returns no ticket they fall back to the identity-wide cursor with a typed warning naming the turn_unavailable code (so on autonomous_host members, the default mode, the wait is identity-wide), and raise instead when nothing was delivered. Do not mix ticket waits and cursor waits on one identity: a ticket can settle before the identity health monitor counts the same turn on completion_cursor, so a later cursor wait can be satisfied by that earlier turn.

Scope-bound dispatch

A host that must never admit a delivery to a session other than the one it recorded captures the identity’s delivery scope, persists it, and dispatches against it. The scoped path never materializes, repairs, retargets or prepares a delivery (no defang, no ambient memory, no injected context), and a lost reply is recovered from the scope’s ORIGINAL session ledger, never from the identity’s current session.

Capturing a scope

meta/status_identity carries delivery_scope, captured observationally (nothing is submitted or materialized):
member is meerkat’s own versioned member scope, which also names the mob’s incarnation; carry it opaque. generation is Core’s member generation and lease_fencing_token Core’s fence token of the live member. A respawn advances the generation, so a scope captured before it is refused as stale, and so is a scope saved by Meta 0.9.3, which held Meta’s own atoms, or one captured at another incarnation of the mob (destroyed and created again under its id). When no scope can be captured (an inactive identity, a member without a session binding, a bridge without the scoped seam), delivery_scope is null and delivery_scope_unavailable is {kind, reason}. Persist the object byte-exact; a scope with a version this gateway or SDK does not read is refused, never guessed at.

meta/dispatch with expected_scope

Pass the saved scope as expected_scope. dispatch_input must carry both idempotency_key and correlation_id; they are used verbatim (no canonicalization). expected_scope does not combine with track_turn. The gateway re-validates every atom: the identity’s runtime id, generation and fence token here, and the member’s session, incarnation and fence inside meerkat’s admission authority, atomically with the admission. The result is the native receipt:
stage is the admission stage the receipt proves; ingress_accepted means the member’s work lane accepted the delivery, not that a durable runtime input exists. A replay of the same key at the same scope is deduplicated onto the original input; a replay with changed content is never a second effect and never overwrites the original (rejecting such a conflict is not part of this release). Every refusal carries data.kind and data.admission_possible: A malformed or unsupported-version expected_scope is -32602 with data: {kind: "invalid_delivery_scope", unsupported_version}.

meta/recover_delivery

Read one delivery’s state from the scope’s original session. Never resubmits, never reads the current session, and works for an identity in any lifecycle state.
The result is {identity, delivery_scope, recovery}. recovery.state is:
  • absent: an authoritative point-in-time miss in the original session. An outstanding admission can still land later; this is never permission to retry.
  • in_flight: the original session holds the input, not yet terminal, with input_id, phase and durable_witness (true only when a committed store row backs it).
  • completed: the delivery’s turn completed, with input_id, output_status, output and output_truncated as for meta/turn_result.
  • failed: the turn reached a failed terminal, with error.
  • terminal_without_run: a terminal disposition without a run of its own, with terminal and last_run_id.
  • broken: inconsistent evidence, with reason.
  • unresolved: the original owner could not establish the state; cause is runtime_adapter_unavailable, original_session_unknown, original_owner_unavailable (with detail), evidence_read_timed_out or scope_binding_unproven (the mob’s member history never bound the scope’s runtime, fence and session together, or the scope names another incarnation of the mob or is a version-1 member scope it does not honour; the session was not read). Never absence.

Retiring at a scope

No method retires at a scope yet. A Rust host retires an identity only at a saved scope with IdentityRuntime::retire_at_scope_tracked (see Retiring an identity at a captured scope). Like meta/retire, it is a release: Core releases the live materialization of exactly the member incarnation the scope names, which stays a member on the same session at the same generation, so the scope stays valid. It never touches a successor minted after the capture, and it retires no spawned_by descendant of the member. meta/retire_member with a generated alias checks the generation only, so it still releases an identity whose session Core moved since the capture.

Keyed sends

meta/send takes an optional caller idempotency_key. A keyed send is committed as a member continuation under the key, in meerkat’s continuation key ledger for the identity’s roster member of the gateway’s mob. The key belongs to the identity, not to a session or an incarnation, and never expires: a retry under the same key never sends twice, after a respawn, reset, wake or gateway restart too. A keyed steer ("steer") is delivered into the identity’s running turn at its next boundary, and starts a turn when the identity is idle. A keyed queue ("queue") runs after the current turn. The mode is part of what the key commits: a replay in the same mode answers the first receipt and never delivers twice, and a replay in another mode is refused as other content is. An unkeyed send queues: it takes "queue", the default, and refuses "steer". The result is {"continuation": {"delivery_id", "committed_at_ms", "status"}} instead of the unkeyed fencing_token and completion_baseline:
  • delivery_id and committed_at_ms are the first commit’s, identical on every replay of the key. status is read for each answer and may advance.
  • status.state is pending (committed, not admitted yet), admitted (with session_id and input_id: queued in that session’s input ledger, not proof that a turn ran; follow it with meta/send_status), stranded (final: the content was never admitted, because the member incarnation it was bound to was replaced; resend it under a new key) or refused (final, never admitted, with reason). Tolerate other values.
  • A pending that survives a gateway restart is not proof of loss: replay the same key, and do not resend the content under a new key.
A keyed send runs the unkeyed send’s own steps: the target checks, the seat of an identity Core does not hold yet (Core materializes a seated member for the delivery), the identity’s lifecycle lock with the alias and registered-row checks, and the delivery preparation. The content is defanged as every delivery is, so a gateway upgrade that changes the defang between a send and its retry turns that retry into a conflict. Ambient agent-memory recall is skipped, with the typed reason keyed_continuation, because a continuation carries no injected context. The content is admitted with the system origin, as every continuation is. Refusals: An InternalOnly identity is refused as by an unkeyed send (-32002). A gateway that predates keyed sends ignores idempotency_key and sends unkeyed. Keyed sends ship with meta/send_status, so check that meta/capabilities lists it before relying on the key. The Python and TypeScript SDKs do, and refuse the key with CapabilityUnavailableError otherwise. A gateway that takes handling_mode on a keyed send lists keyed_send_handling_mode in meta/capabilities.feature_capabilities. An older one ignores the mode and commits a queued send, so check for it before a keyed steer. The SDKs do (send(..., idempotency_key=..., handling_mode="steer") in Python, send(identity, content, { idempotencyKey, handlingMode: "steer" }) in TypeScript), and refuse the steer with CapabilityUnavailableError otherwise.

Which key to use

Prefer a keyed meta/send for host sends that must survive retries over the identity’s whole life.

meta/send_status

Read the state of an input in a session the identity has had: its current session, or one meerkat’s member history binds to it. A keyed send’s admitted pair is followed with it.
The result is meerkat’s session/input_status result unchanged: {"state": {...}} with the input’s input_id, current_state, terminal_outcome once it is terminal, and the rest of meerkat’s input state, or {} when the session holds no such input. The target checks are meta/send’s, in its order. An InternalOnly identity is refused with -32002, and a session the identity never had with -32602 and error.data {"kind": "session_not_held_by_identity", "identity", "session_id"}: the read is never a free session read. The console serves it as an identity read, behind the agent.view grant on the identity.

WorkGraph methods

The meta/workgraph/* group exposes meerkat’s WorkGraph (the shared-work ledger) for this runtime’s realm. Results serialize meerkat’s typed results verbatim (WorkItem, WorkGraphSnapshot, WorkAttentionBinding, WorkEdge, …); expected_revision is the compare-and-swap token for revision-checked item/binding mutations, as specified by each method below. Item/goal creation, linking, and terminal attention pruning do not require it. All fields are snake_case. Availability and the auto-issued grant. The group is advertised in meta/capabilities (and "workgraph": true is set) only when the service is configured, which it is by default: runtime_options.workgraph defaults to enabled (Python .workgraph(True), TypeScript .workgraph(true); false disables, a string is an explicit durable-store directory). With persistent_state the store is <persistent_state>/workgraph.sqlite3; without it the service is memory-backed and the gateway warns at boot on identity-first launches. The WorkGraphNamespaceGrant meerkat requires per agent is issued by Meta from the wired service (realm = Meerkat’s canonical mob.<mob_id>, namespace default) for every member whose profile sets tools.workgraph = true; hosts do not issue it. realm_id is rejected (-32602) on every method for the same reason: the service is realm-scoped at construction and agent tool calls are scope-pinned to it. Filter semantics that are easy to get wrong.
  • labels matches ALL: every listed label must be present on the item (list, ready, snapshot). This is the opposite of the builtin task_list tool, which matches any label.
  • include_terminal defaults to false and drops completed, cancelled, and failed items even when statuses names one of them; statuses: ["completed"] without include_terminal: true returns nothing. The bundled console panel passes include_terminal: true, so it can show items the SDK default hides.
  • limit defaults to 100 and may not exceed 1000 (-32602 beyond it).
  • Nothing deletes items or edges. Close an item as cancelled or failed, or link a replacement with kind supersedes; attention/prune is the one deleting verb and removes only terminal attention-binding rows. The event stream keeps the history.
  • A parent edge points from the child (from_id) to the parent (to_id); use blocks only when the target must not be ready until the source is terminally resolved.
  • close without status records completed. Pass status explicitly when the outcome is anything else.

Read methods (ABAC workgraph.view)

A list, ready or snapshot page that ended early has truncated: true and a next_cursor; pass it back as cursor (same method) for the next page. A cursor of another method, of another version, or one that does not decode is -32602. anchor_event_seq is the ledger’s high-water mark when the walk began, and changed_since_anchor is true once the ledger moved past it: meta/workgraph/events with after_seq set to it names what changed. An events page that ended early names next_after_seq to pass as after_seq. The paging rules are Meerkat’s (docs/api/rpc.mdx, workgraph/list).

Mutating methods (ABAC workgraph.manage; console also requires can_mutate)

goal/create, attention/*, and policy/escalate reject a non-default namespace (-32602) because upstream turn-overlay resolution reads only the service’s default namespace. One active (or paused) binding per target is enforced across goal/create, attention/reassign, and attention/resume; a second returns -32042 naming the occupant. Member-bound targets are realm-bound. A mob member resolves its attention bindings in its mob’s realm (mob.<mob_id>) and nowhere else, and this runtime’s service is scoped to its own mob’s realm, so a target that lowers to a member of this mob ({kind:"identity"}, a member session, or the member’s owner key) is stored where the member reads it. A target whose owner key names a member of another mob is refused with -32602 and data.kind = "attention_target_realm_mismatch" before anything is written; create that work through the other mob’s own runtime. Bindings that predate this rule are moved at bootstrap; meta/capabilities reports the result under workgraph_realm_migration.

Errors

SDK method names are the RPC names with the meta/workgraph/ prefix dropped, in snake_case for Python (workgraph_snapshot through workgraph_attention_prune, plus workgraph_add_evidence and workgraph_escalate_policy) and camelCase for TypeScript. attention/break_glass_reassign is deliberately in neither SDK, and meta/workgraph/facts (the cursor-tail projection behind the SSE wake stream) has no SDK wrapper either.

Notifications

The persistent rpc_gateway sends JSON-RPC notifications (no id) to the SDK alongside responses. Callback requests (callback/*) expect a reply; these do not.

meta/init_progress and meta/init_settled

Sent only to an SDK that opted in to the accepted-then-settled init. See Accepted then settled.

meta/on_error

Every runtime ErrorEvent, serialized with its internal category tag in snake_case and the variant’s fields beside it:
rpc_gateway installs the forwarding hook unconditionally, so the runtime’s absent-hook notice never fires for SDK hosts; the SDKs deliver the payload to the callback registered with Python .on_error(callback) / TypeScript .onError(callback) and drop it silently when none is registered. The same event is always logged on the gateway’s stderr at ERROR (INFO for actor_loop_recovered and mob_announcement_escalation_resolved). meta_gateway emits no notifications. The variant table is in Error hook and logging.

Operator transcript verbs

Identity-scoped operator verbs for member transcripts (meta/compact_member, meta/bound_member_transcript, meta/prune_member_system_rows) and a member’s instruction activations. The transcript verbs resolve their target through the same identity-control gate as meta/respawn / meta/reset, so identities the gateway does not own are refused at resolution.

meta/compact_member

Force one transcript compaction on a member, then restore its original compaction threshold. Arms a temporary auto_compact_threshold floor for the identity, respawns the member in place so the fresh build picks the floor up, drives one queued maintenance turn (the forced compaction fires at that turn’s pre-LLM boundary), then disarms the floor and respawns again. Each in-place respawn retires the member, which ends its wiring, so Meta wires the identity’s managed edges (the roster’s and the topology controller’s) again once the member is woken, bounded; a re-wire that fails is logged and the next reconcile retries it. Params: identity (required), floor_tokens (default 1024), timeout_ms (default 60000, budget for the maintenance turn). timeout_ms bounds observation, not execution. The operation submits exactly once with a caller-owned idempotency key and reads that input’s rich finalized completion through the persistent session owner’s runtime adapter. A completion cursor, Consumed input phase, or accepted interrupt is not completion proof. After the deadline, the RPC remains an error even if compaction completes later. The existing five-second secondary observation budget allows exact late completion and profile restoration to be reported. If it expires, error.data reports kind: "compact_member_timeout", stage: "terminal_pending" and an operation_id; the runtime still owns completion and profile cleanup. Dropping the RPC caller does not drop that ownership. Read failures likewise report pending/unknown fate and retain the same input’s observer, without resending. Concurrent compaction of the same identity refuses while that operation owns it. An uncertain admission reply follows the same original key through these reads; it never causes a second submission. The pending-operation record is separate from the build override, which applies only to the intended maintenance build. Only an exact successful terminal for the captured session and incarnation authorizes clearing the floor and rebuilding the same session. A failed terminal preserves its completion_type and typed error metadata; supersession refuses restoration rather than reviving a retired identity or touching its replacement. Supersession invalidates only that operation’s obsolete override; it does not rebuild the member or let the old floor leak into a later incarnation. No timeout triggers retirement, transcript discard, or a synthetic healthy state. Stock persistent compositions supply the exact observer. Custom compositions must forward CommittedBoundaryRecoverer::runtime_completion_observer or use MobBootstrapSpec::with_runtime_completion_observer with the concrete persistent session service’s canonical runtime adapter. This is separate from the bootstrap adapter override. Missing capability returns -32016 before installing a floor; an observer-only injection does not claim recovery support. Result: identity, session_id, floor_tokens, messages_before, messages_after, compaction_applied, head_revision, last_rewrite_reason (message counts and revision facts are null when the gateway has no concrete transcript-edit service).

meta/bound_member_transcript

Surgical escape hatch: commit one audited keep-last-N transcript rewrite on a QUIESCED member session (running sessions refuse with -32015; retire or park the member first). The cut point is pair-safe: it never separates an assistant tool-use message from its adjacent tool_results. The removed prefix is replaced by one operator marker notice, and the rewrite is committed with a compare-and-swap against the head revision observed at read time. Params: identity (required), keep_last (default 50), note (optional audit note recorded on the rewrite reason). Result: bounded, removed, kept, message_count, parent_revision, revision (or bounded: false with removed: 0 when the transcript is already within keep_last).

meta/prune_member_system_rows

Host-only. Removes the ordinary System rows the host names from the member’s current session in one audited rewrite (Meerkat’s SessionServiceTranscriptEditExt::prune_system_rows), and keeps every other row: the precise remedy for stale prompts that earlier boots wrote. The member is addressed by identity through the same identity-control gate as meta/bound_member_transcript. A dormant member is not woken and no model is called; an idle live member’s session converges on the commit, and its next turn reads the pruned transcript. The SDKs call it as MetaRuntime.prune_member_system_rows (Python) and MetaRuntime.pruneMemberSystemRows (TypeScript), and export SYSTEM_ROW_PRUNE_REFUSAL_CAUSES, the closed vocabulary of a refused row’s cause below. Params:
  • identity (required).
  • remove: the rows to remove, each {index, content_sha256} as a member history read showed it: index is the row’s 0-based message index (the index space of the history read), and content_sha256 is sha256:<64 lowercase hex> over the row’s content as UTF-8 text. Gateway and Meerkat never choose the rows: read the history, pick the rows, send them. Only ordinary System rows are removed, and nothing guards the last one: the host chooses.
  • retire_superseded_activation_bodies (default false): the same commit also retires the body of every instruction activation row that a later activation of its key superseded, in place, as a compaction does; a key’s newest body is never retired.
  • expected_parent_revision (optional): an extra compare-and-swap on the transcript head. The per-row digest is the main guard: a row that changed since the read is refused. A stale revision is refused too (see Errors), so chaining calls on the revision a previous call returned fails whenever a turn ran in between: read the history again rather than chain.
  • note (optional): the audit note. The commit’s reason kind is always system-row-prune; there is no caller-supplied reason.
The rows after a removed one move up. An instruction activation row keeps its place among the conversation rows; a prune that moves one to another offset, or retires a body, writes the session document at envelope 5, which Meerkat 0.9.8 refuses, while a prune of rows after the last activation row with nothing retired leaves it at envelope 4. The session’s pending compaction projection intents carry forward unchanged, as for every same-session rewrite, so an unsettled projection does not refuse the prune. The commit replaces one range, from the first removed row to the last retired one, and its rewrite record keeps that range’s earlier rows for the session’s rewrite retention: with the retire option on a transcript whose stale prompts open it, that is most of the transcript, a one-time growth of the stored document. Result: identity, session_id, pruned, removed_rows, retired_bodies, message_count, revision, and parent_revision when pruned. pruned is false (with zero counts) when the request names no row and there is no body to retire; nothing is written then. Errors:
  • -32602 with data {kind: "system_row_prune_refused", index, cause}: the row at index cannot be removed, and nothing was written. cause is out_of_bounds, duplicate_index, not_a_system_row, prompt_version_row, instruction_activation_row, operator_instruction_entry_row, digest_mismatch (the transcript changed since your read: read it again) or before_operator_instruction_entry (an operator-instruction entry follows the row in its group of System rows, and removing the row would move the entry; a group of System rows is a run of consecutive System rows, which any other row, a system notice included, ends). SystemRowPruneRefusedError in both SDKs.
  • -32602 with data {kind: "transcript_revision_conflict", expected, actual}: expected_parent_revision is not the session’s current revision actual, because a turn ran or another edit committed since the read; nothing was written. Read the history again. TranscriptRevisionConflictError in both SDKs.
  • -32015: the member is running a turn or holds an active runtime admission. Retry when it is idle.
  • -32603 with the data a held wake answers: a session Core holds (a durable tail held for recovery, a refused recovery, quarantined evidence) answers {kind: "mob_member_session_resume_held", durable_resume_hold, ...}, and one whose committed document fails its audited-endpoint check {kind: "mob_member_session_repair_required", operator_path, ...}; the session is kept and nothing was written. IdentityWakeHeldError in both SDKs.
  • -32047 candidate_launch on a candidate launch.
  • -32016 when the gateway has no concrete transcript-edit service.
  • -32000, the catch-all, prune_member_system_rows failed: <cause> or a message naming the state: an identity with no current session, a member placed on another host (the gateway’s transcript service holds no session for it, as for meta/bound_member_transcript; no operator verb has a typed refusal for a placed member yet, and one waits for the member-host bridge to carry the request), an archived session, or a malformed session document. Read the history again before you retry.

meta/member_activate_instruction

Host-only: present on the gateway’s JSON-RPC surface and the SDKs, never in an agent tool catalog. It changes a member’s standing instructions after its session was minted (resume authors nothing, so a restored member otherwise keeps the prompt it was created with). Params: identity (resolved through the same identity-control gate as meta/respawn and meta/compact_member) and activation, Meerkat’s InstructionActivationRequest verbatim: revision (namespace, key, revision_id, content_sha256 = sha256:<hex of body>), activation_id, expectation ({"kind": "absent"} for the first activation of a key, then {"kind": "effective", "activation_id": ...}), optional supersedes, and body. The activation is appended forward to the member’s CURRENT session through Meerkat’s MobHandle::activate_member_instruction: it holds the session’s turn-finalization boundary, applies the runtime admission, and adds one keyed transcript row that the member’s next turn sees. History is never rewritten, and the activation survives resume. A call made while the member is mid-turn blocks until that turn finalizes, so it can wait for a whole turn. It then applies at that boundary, never inside the turn, or is refused as session_busy if more runtime work is already queued there. The gateway applies no read deadline to this verb. Give the client side a reply timeout that covers the member’s longest turn (the SDKs’ transport_timeout / transportTimeoutMs). If the client gives up first, the activation may still apply at the boundary: re-send the identical activation, which answers applied, duplicate or the typed refusal. The result is {identity, receipt}; receipt.disposition is applied, or duplicate when the exact activation is already effective. Errors keep Meerkat’s classes distinct in data.class:
  • admission, with data.instruction_activation_code: session_busy, live_channel_open, target_not_materialized, external_write_fence_conflict and external_write_fence_backoff answer -32015 (nothing was appended; retry at your own next boundary); unsupported_current_lowering answers -32602; durability_unavailable answers -32603.
  • session, with data.session_error_code (Meerkat’s stable session error code). When the session owner refused the request itself, data.instruction_activation_error_code carries Meerkat’s typed code and the error is -32602: invalid_request, digest_mismatch, effective_activation_conflict (a stale expectation; re-read key_state), activation_identity_conflict, immutable_revision_conflict, recorded_but_not_effective, or inherited_activation_requires_explicit_activation. malformed_activation_history answers -32603. A busy session answers -32015; any other session error answers -32603.
  • runtime and owner_task: the runtime authority or the owning task failed.
Nothing retries inside the gateway. An identity with no member session is refused as admission / target_not_materialized, with nothing appended. A fork of the member copies its activations only as history: the gateway’s runtime_options.fork_instruction_policy decides whether the child gets its own activation of each key, or whether the fork is refused. See Fork instruction policy.

meta/member_instruction_activations

Host-only. Reads the durable instruction activation records of the member’s current session. Params: identity, and optionally namespace, key, offset and limit (default 100). The result is {identity, page} with Meerkat’s InstructionActivationReadPage; naming one exact namespace and key also returns its key_state, including the next_expectation for the next activation.

Scheduling

meta/schedule/tools

Lists the definitions of Meerkat’s durable schedule tools, the meerkat_schedule_{create,get,list,update,pause,resume,delete,occurrences} tools that a member whose profile sets tools.schedule = true is given. The result is Meerkat’s own schedule/tools result, unchanged, so a client can read the authoring and readback schemas from the gateway it talks to. The answer is static. The gateway serves it from Meerkat’s definitions without opening a schedule store or starting a schedule host, and it ignores params, as Meerkat’s schedule/tools does. These are the base definitions: inside a member’s session, meerkat_schedule_create and meerkat_schedule_update also accept a session target of type: "current_session", which the tool host resolves to a durable target before it persists the schedule. Whether this gateway attached the schedule tools is a separate fact, reported by the schedule slot of the storage summary in meta/status and meta/capabilities. Available on the unified stdio and console (POST /console/rpc, read method, no grant) surfaces. The module-only surface, the aggregator-only console and the Flow Editor do not serve it. Additive method, no contract_version bump. The SDKs read it with MobHandle.schedule_tools() (Python) and MobHandle.scheduleTools() (TypeScript).

Console schedules (Console)

meta/schedule/list, meta/schedule/create, meta/schedule/update, meta/schedule/pause, meta/schedule/resume and meta/schedule/delete keep an agent’s console schedules: messages a Console caller defines once and the Console sends to the agent later, at the times of a durable schedule in the gateway’s schedule store. Served on POST /console/rpc only, as meta/fork_member is. The Console’s meta/capabilities lists meta/schedule/list where the runtime’s schedule host keeps the store, and the writes where the console is also writable and a schedule firing host sends the gateway’s schedules. Every method names the agent as identity, as meta/console/send does. Additive methods, no contract_version bump. The list needs agent.view on the agent and every write needs agent.send (-32030): a schedule is a deferred send. A write takes exactly the gate meta/console/send takes on this route, for the caller as the console resolves it, signed in or not: a writable console (-32010), a valid public alias, and agent.send under the live access policy. A candidate launch refuses the writes (-32047).
content is the message, a non-empty string. trigger is Meerkat’s schedule trigger, as the schedule tools take it, tagged by type: once (due_at_utc), interval (start_at_utc, every_seconds, optional end_at_utc) or calendar (timezone and the minute, hour, day_of_month, month and day_of_week fields, each {"kind": "any"} or {"kind": "values", "values": [...]}). An interval repeats at most once a minute (every_seconds of at least 60). An update keeps each of content, trigger and name it does not name.
meta/schedule/list answers {identity, schedules}, the agent’s console schedules, oldest first. Create, update, pause and resume answer the schedule; delete answers {identity, schedule_id, deleted: true}. The sender. A create, an update and a resume record the caller as the schedule’s sender: the principal the console’s send would use for that caller (a verified signed-in human, another authenticated principal, or the open Console’s shared account), with the provider and issuer its token verified under, and the subject only for a verified human. A pause or a delete leaves the sender as it is. A resume records its caller while the schedule is still paused, so no send goes out as the earlier sender. Only a caller who may send to the agent sees who its schedules send as: a caller who may only view it gets sender: null and sender_hidden: true. Each fire is a console send by its sender. At each due time the Console first admits the recorded sender as it admits a live request by it, under its decisions then: an anonymous sender only while the Console requires no sign-in; a principal only under the issuer the Console trusts then, past its provider policy and allowlist where sign-in is required, and past its session fence, so Console sessions revoked (meta/console/revoke_sessions) after the schedule was defined end its sends, for a service identity as for a human. It then sends content to the agent exactly as meta/console/send does once its caller is admitted, as the sender: the candidate-launch refusal, a writable console, agent.send for the sender under the access policy at that time, an addressable agent, and governance. The send is deduplicated under its occurrence’s delivery key in the sender’s send scope, as a live send by the sender is, so a redelivered occurrence sends once. It reaches the agent with origin console:schedule and the scheduler turn origin. Changes made meanwhile. An update and a resume write at the revision they read: one that raced another change answers -32009 (schedule_revision_conflict) and changes nothing, so it never puts back what the other change replaced. Read the schedule again and repeat it. A refused or failed send fails that occurrence (delivery_failed in the schedule store), and the schedule keeps its later times. That covers a sender who lost agent.send, a console that turned read-only, and an agent the console no longer knows (a deleted identity, or a member without one that was retired): the schedule stays listed until it is deleted. An identity released with meta/retire is materialized by the fire, as by any console send. Known limits:
  • Governed agents. An agent whose input is governed takes no console schedule: the create is refused (governed_target), and the fires of a schedule whose agent became governed fail the same way.
  • The agent’s own schedule tools. Console schedules are the Console’s: an agent’s schedule tools neither list nor change them (a verb on a schedule whose kind they cannot read is refused), they cannot create a schedule that names the host runnable (meta.console.scheduled_send), and an application cannot register a host runnable or callback under that name. A schedule a Rust embedder writes to the store directly is not checked. The Console lists console schedules only.
  • One Console sends them. Fires go through the Console router built last, under its decisions and visibility policy. A schedule host on a gateway that serves no Console and drains the same store fails their fires: a fire waits for a Console only in the first 30 seconds after the gateway’s schedule host starts, and otherwise fails at once. Replacing the Console’s log store, and shutdown, end the fires until a Console router is built again.
  • Dedupe across a restart. A gateway whose console send dedupe store is in memory forgets a fire’s key when it restarts, so an occurrence redelivered after a crash can send twice.
  • Unobserved admission. A fire whose admission the Console could not observe (a timeout) is recorded failed, although the message may have been admitted; it is never sent twice under its key.
  • No fire history in the Console. A failed fire is recorded on its occurrence in the schedule store and in the gateway’s log, not in the list.
  • Edits from an older view. An update is checked against the revision the gateway read for that call, so an edit made from an older view of a schedule still replaces a newer one.
  • Names reused. A schedule of an agent that is deleted stays until it is deleted, and fires into a new agent declared later under the same name, as its sender, while that sender may still send to it.

Scheduling migration

Contract 0.5.0 removed meta/scheduling/evaluate and meta/scheduling/dispatch. Both names now return the standard -32601 method-not-found error. Use the durable schedule tools and ScheduleService, which own schedule definitions, occurrence claims, firing records, and target delivery. See Scheduling for migration guidance.

Session store methods

meta/session_store/bigquery

Execute BigQuery-specific session store operations.

Runtime status

meta/status

The runtime’s status. On the unified runtime the result carries contract_version, running, loaded_modules and mob_state, plus http_base_url, identity_bootstrap, storage and detached_jobs when the runtime has them. With mob announcements enabled it also carries mob_announcements: their sources, members and escalations at a glance (see Mob announcement health). On an identity-first runtime lifecycle_projection says whether the side effects of the identities’ lifecycle steps run (the customizer’s after_create, the host’s retirement hook, a retired session’s distillation and exit interview), as {state, owed_events?, detail?}. state is pending (not started yet, or fewer than three reads of its cursor in a row failed), running, stalled (its last three preparations, or the last three runs of one of its lanes, failed: the side effects they owe wait), inactive (a candidate launch, or a runtime without a session bridge) or off (the session bridge cannot read Core’s member lifecycle journal, or an identity’s binding, so no lifecycle side effect runs). owed_events counts the journal events whose side effects a stopped predecessor still owed when this runtime started, which run in the background; detail says why the projection is off, or what its runs keep failing on. The SDKs pass the field through untyped. member_restore says what the mob’s latest start materialized its members from: {"boot_set": "observed"} when the runtime store answered which members hold work, so the start materialized only the members that must be live (members with work, a network endpoint, a host requirement, or all of them under runtime_options.member_restore = "eager"), or {"boot_set": "unobserved", "unobserved_cause": "recovery_scan_unsupported" | "delivery_listing_unsupported" | "scan_failed"} when the store could not say (scan_failed: its scan failed, and the start went on), so the start materialized every member. The meta/init result carries the same object, and the gateway logs a warning at start when it is unobserved. mob_state is the last lifecycle phase the mob’s actor published. An actor that fail-stopped keeps that phase, so a stopped mob can still read a live phase there. mob_failure is present only once the mob’s actor has stopped: it says why (the failure the actor recorded, or the error of an actor that ended while its phase was live). Such a mob admits no command until a restart recovers it from its durable state. broken_members is present only when Core holds members Broken: members it could not restore at this start, for example because a restored member’s comms identity is missing on this host. They serve nothing until they are repaired, so a readiness probe can read the list; /healthz reports the same list in JSON.
quarantined_mobs is present only when durable child mobs are on and the store of one or more of them did not restore at this start. Meerkat keeps such a store quarantined: kept exactly as found, with no actor, and every call that addresses the mob answers the mob_quarantined refusal. It does not fail the boot, and the mob stays in the hosted set but does not run. A store_unavailable mob is restored again by the next call that addresses it and by the next start; repair the store of any other class, then restart (see Meerkat’s mobs guide). quarantined_mobs_error replaces the list when the restore of the child mobs failed as a whole. A quarantine of the gateway’s own mob fails the boot instead, with the typed mob_quarantined refusal as the reason.
reserved_identity_members is present only when a member of the mob holds an identity the console reserves for its own events (_system or __console__). No Meta creation path admits such a member; one created before that check, or by host code through a mob handle directly, keeps running, but the console attributes none of its events to the reserved identity. Retire it and create it under another name.
non_expiring_console_principals is present only when the Console accepted a token without exp for some principal. Such a token never expires; only a revocation (meta/console/revoke_sessions) ends it. Each entry names the principal and the flag, never a token.

meta/console/revoke_sessions

Revoke every Console session of one principal as of now. From then on the Console refuses with 401 each token of the principal whose iat is earlier than the returned not_before plus 30 seconds (the verifier’s clock leeway), or that has no iat, and ends every event stream and console voice session such a token opened. A later revocation moves the fence forward; an earlier one never moves it back. Errors: -32602 for a missing or empty principal; -32004 when the runtime has no durable session fence record (no persistent state directory), so the revocation would not survive a restart; -32603 when the record could not be written (nothing changed), or when, on Unix, its directory could not be synced after the record’s file was replaced: the revocation then took effect but may not survive a power loss, and revoking again confirms it. The console’s /console/rpc serves the same method. With an access controller, enforced or not, only an access admin may call it, as for the meta/access/* edits; on a console without one, any caller a token identified may, as with other console actions. A read-only console refuses it.

Storage methods

meta/storage/doctor

Read-only storage diagnosis over a Meta state directory (the diagnose half of Meerkat’s StorageMigrator seam). Safe against a live gateway: read-only SQLite opens, no file creation, no leases, no schema-ledger runs. Reports the per-directory database inventory (with schema-ledger versions), file-name twins (sessions.db beside sessions.sqlite, continuity.db beside identity_continuity.sqlite, agent-memory/ beside agent-memory-sqlite/, …), the continuity checkpoint-evidence census per identity, dangling console-frame blob references, legacy-FS blob objects, and filesystem artifacts. On a live persistent gateway the H1/H2 durability resolution (blob-durability, session-store-incremental) is attached as findings; a cold directory reports durability-census-unavailable. Available on the module-only, unified, and console (POST /console/rpc, read method, runtime.admin grant) surfaces. Additive method — no contract_version bump.

Subscribe methods

meta/events/subscribe

Return a bounded replay snapshot of merged runtime events. The result includes both typed event envelopes and the same events rendered as SSE frames; this method does not itself open a live HTTP stream.
Scopes have these meanings: The server applies the scope first, then retains at most the three newest matching events. With no last_event_id, all retained events are returned. With a checkpoint, replay starts at that event, inclusively. A checkpoint that is unknown, belongs to another scope, or has fallen outside the three-event window returns JSON-RPC -32602. A reconnecting consumer should deduplicate the first replayed event by event_id. The scope selects from the runtime’s buffer of recent merged events, which keeps at least the newest 4,096 and drops older events as new ones arrive. An agent or interaction scope whose newest matching events are older than the buffer returns fewer events, or none, and a checkpoint that has left the buffer has expired like any other: call again without last_event_id.

Console ingress

The older console.route style is not the stock console contract. Use GET /console/experience, GET /console/timeline, and POST /console/rpc instead.

Mob announcements (optional)

Available on hosts launched persistent and identity-first with mob announcements enabled: runtime_options.mob_announcements = {"enabled": true} on rpc_gateway, the mob_announcements init param on meta_gateway, or UnifiedRuntimeBuilder::mob_announcements in a Rust host; otherwise these methods answer -32044. The console serves the same methods on POST /console/rpc through the same handler, so a runtime without mob announcements answers them there exactly as rpc_gateway does: -32044 mob_announcements_unavailable. On the console every method, the reads included, requires the runtime.admin grant, and a read-only console refuses the ones that change state (announce, subscriptions/{put,pause,resume,cancel}, deliveries/{reconcile,redeliver,dismiss}, sources/{put,pause,resume,end,advance,record_failure} and declarations/apply). A candidate launch (runtime_options.mob_composition.authority = "candidate") answers the reads but delivers nothing, keeps no source runner schedule, and refuses each of those changes with -32047 candidate_launch (a member tool’s change with a tool error naming the candidate launch): queued work and changes belong to the authoritative launch. On the console, subscriptions/put refuses a member whose identity is _system or __console__ (-32602), the identities the console reserves for its own events. The host announces a development to a mob hosted on this gateway, once per mob and source key. Every member subscription that names the mob and one of the announcement’s topics captures it in the same transaction, and each subscriber receives what it is owed in bounded batches, each as one Meerkat host continuation. The design, defaults and limits are in docs/design/mob-announcements.md in the repository. mob_id and mobs default to this gateway’s mob, and every mob named must be hosted here (-32048 unknown_mob otherwise). A host call announces as {"kind": "host"}; captured counts the subscriptions that captured the announcement and never names them. start is {"kind": "now"} (default) or {"kind": "after", "frontier": {"<mob id>": <sequence>}}; capture starts after each named sequence, and the frontier may name only the subscription’s mobs. A sequence beyond a mob’s head is refused with -32046 frontier_beyond_head, never clamped. native.status is the native continuation owner’s own status (not_submitted, not_committed, pending, applied, stranded, refused, or unavailable when it could not be read), and input is the admitted input’s runtime state. Admission is not processing: read input for what the runtime did with it, and handling for what became of the turn (see Delivery handling). A batch in state expired waited longer than the retention window with nothing native holding it, and, once submitted and owed again after a refusal, past the quiet window after its last request. One in state expired_unverified was submitted, and whether it was committed was never learned: past the retention window, and past the quiet window since its last submission (the request window plus the slowest settle pause), no status read brought a status. One in state committed_unconfirmed was committed to the member’s inbox, with its receipt kept, and its admission was never confirmed: past the retention window since the commit it was still pending, or no status could be read for the quiet window after that. Neither is ever submitted again. The member’s next batch lists what it did not get in gaps ([{mob_id, count, reason}], reason expired, not_retained, delivery_not_confirmed or admission_not_confirmed) and in a gap line of its body. A delivery that was not confirmed can still arrive later, after later announcements: neither its arrival nor its absence was confirmed. A batch that settles so hands its own gap lines to the member’s next batch too: a member reads them at least once, and may read them twice. What a status read finds after such a batch settled is kept on it as late: the native status in its own shape (status is not_committed, pending, applied, stranded or refused, with its receipt), and at_ms, when it was read. It never reopens the batch, and the member’s next batch goes on as it was. Such a batch is read again once per quiet window until what it keeps is final (anything but pending) or retention removes it. A put names exactly one of member, one member, and members, every member of a mob with a role (a role template). delivery is {"await_handling": bool}: whether the member’s next batch waits until the turn of its previous one has ended, for at most 15 minutes. Left out, it is on for the triage member of a triage route that takes a topic the subscription selects, and off for any other member. A subscription answers it as delivery when it was set. member is {"mob_id": "<mob id>", "identity": "<identity>"}: an identity is unique only within its mob. A member of this gateway’s mob must be a registered identity served by this gateway’s runtime: one placed on a host or bound to an external runtime is refused with -32047 unusable_member. A member of another mob hosted here (a mob a member created) must be on that mob’s roster now, and the mob running and not completed; it is reached through this gateway’s runtime by its plain roster id. A mob held until its owner resolves is native_unavailable. A mob id that names a mob hosted here and a peer alike, or a member whose kept route would change, is refused with ambiguous_mob. A member of a mob on a peer gateway is reached through keyed inject (see Keyed inject) when this gateway’s contact directory files that mob with a pinned key and this gateway holds a signing key; otherwise it is refused with unusable_peer. A subscription’s state is active, paused, cancelled or ended. It ends, with ended saying why, only on a final fact: every mob it named was destroyed (mobs_ended); for a member of another mob hosted here, that mob was destroyed, created again, completed or reset after the subscription was made (member_mob_ended, with why); or its member departed (member_departed, with the departure evidence: for a member on a peer gateway, that gateway’s signed member_departed answer, {"kind": "signed_peer_departure", "peer": "<mob id>"}). No gateway of this release records a final fact that a member left, so none answers member_departed yet, and a member’s departure ends nothing. What an ended subscription captured and no batch holds settles. A destroyed mob leaves the subscriptions that name it at a new revision and is listed in their ended_mobs. A member that is not registered, or not served, where its deliveries go ends nothing: it may be registered or served again. Nor does a member that left this gateway, a placement, a governed runtime or a member with no live incarnation. Its owed batches expire after the retention window meanwhile, and subscriptions/cancel ends a subscription whose member is not coming back. Its deliveries wait, and marker says why: the latest classified outcome of an attempt for the member, {cause, evidence, since_ms}, held in memory and rebuilt from the next attempt after a restart (null, or absent, while nothing waits). It is the latest attempt’s, so it can outlive the wait it names (after a cancel, or once the batch settles) until the member’s next attempt or a restart. evidence is verified (a signed answer whose signature verified), unverified (an unsigned answer, one that failed verification, or none) or local (this gateway’s own fact). A signed answer that nothing was committed returns the batch to sealed, and it is submitted again at the slowest settle pace; an unknown outcome keeps it submitting until a status read settles it, and nothing is submitted again before one does. A not_served or stale_request cause this build does not know reads unknown; a peer’s refusal code it does not know is kept as peer_refused_unknown with the code; and a departure’s evidence has one kind in this release, signed_peer_departure. On the peer route, a peer answers its own blocked status (native_key_invalid, native_inconsistent) as native_unavailable, so the batch waits instead of blocking and the bounds on an unknown outcome settle it: a status read that rules a batch out blocks it on the local route only. A blocked batch’s cause starts with its typed reason: conflict, peer_invalid_request, request_oversize, native_invalid, native_inconsistent or native_key_invalid.

Delivery handling

Admission is not processing, so Meta reads what became of each admitted batch’s turn. Once a batch is admitted, Meta reads the admitted input’s terminal receipt until the answer is final, at the settle pace. Without a receipt, it reads the input’s terminal outcome and abandon reason. deliveries/list entries carry the answer as handling, and so do deliveries/observe and deliveries/reconcile. It is null until the batch is admitted, and otherwise has this shape: {state, cause?, redelivered, held, dismissed, read_at_ms}. A turn that has ended but whose receipt Meerkat has not finalized yet stays pending until the receipt says how it ended. A graceful shutdown does not end a durable member’s turn: Meerkat keeps it for the member’s next start, and its batch is read there. A kept turn that completes there is handled, and nothing of it is redelivered. If the member’s next start comes after the stop’s requeue max age (15 minutes from the input’s admission), the turn ends unhandled with the cause requeue_expired. A kept turn that is settled instead of resumed ends unhandled with the cause resume_refused. A policy decides what happens to the items of an unhandled turn. For a source item, the policy is its source’s delivery.on_unhandled. For a plain announcement, it is on_unhandled in runtime_options.mob_announcements (see SDK and Gateway runtime options).
  • redeliver is the default for source items. It owes each item again, and the member’s next batch carries the item first, under a notice. The notice names the earlier batch and how its turn ended. It lists each item by mob, sequence and key, plus a source item’s item id, stream and version. It warns that what the turn started may already have happened. Processing is at least once: a turn interrupted after partial progress (a cancelled run, a retired member, a stop that ends the turn) is unhandled even when its batch, or part of what the turn did, already reached the member’s transcript, so the member can see those items more than once. redelivered counts these items. An item is redelivered automatically at most twice.
  • hold keeps the items for an operator, and held counts them. Some items are held whatever the policy says: those past the automatic redeliveries, and those whose turn ended in a reset, an interrupted tool process or a refused resume. A member that holds items escalates as unhandled (see Mob announcement health).
  • report is the default for plain announcements, as in 0.9.4. It only records and counts the outcome.
An operator has two remedies for what a batch holds:
  • deliveries/redeliver checks each held item’s coverage afresh. It owes the items that pass again, with the notice. An item that fails waits on coverage, like any item its member does not pass.
  • deliveries/dismiss settles the held items as handled by an operator. They are never delivered again.
Both take an operation_id, like every change, and answer the batch’s handling. For a batch that holds nothing for an operator, both answer -32047 not_held with batch_id. audit/list counts each hold (items_held), redelivery (redelivered) and dismissal (dismissed) with the reason unhandled. An entry for a plain announcement has no source_id or generation.

Mob announcement sources

A source is a durable record, per mob and source_id, of one external scope a collector observes; see Sources. A gateway that serves sources lists mob_announcements.sources.v1 in meta/capabilities.feature_capabilities; one that serves mob announcements without sources lists the methods above but not that capability, and an older one answers the methods below with -32601. Every method takes mob_id?, this gateway’s mob by default, and every one that changes state takes an operation_id: the same id with the same request returns the original answer, and with another request it is refused operation_reused. sources/put creates a source when expected_revision is absent and replaces its configuration at that revision otherwise; with "new_generation": true it ends the current generation and starts the next in the same transaction. Its fields:
  • identity is {provider, principal, scope: {kind, id}} and is fixed within a generation (identity_immutable). principal is the account the collector reads as: {"kind": "connector_verified_account", "issuer", "strategy_id", "subject"}, the identity a connector login verified, or {"kind": "host_attested", "attester", "account"}, which only a {"kind": "host"} connection may carry (principal_not_allowed otherwise). Two sources of one mob cannot share a provider, principal and scope (source_identity_taken).
  • connection is {"kind": "connect_setup" | "host", "id"}, a reference Meta keeps and never dereferences.
  • observation is {"baseline": "delta" | "snapshot", "deletions": "explicit" | "unobservable"}: how the adapter takes the provider’s position, and whether it can see deletions.
  • topics (1 to 16) are owned by the source in its mob (topic_owned for another source’s, and a plain announce on one is refused topic_owned_by_source).
  • audience lists who may receive its items, as grants over roster facts: {"kind": "member", "member"}, {"kind": "role", "mob_id", "role"} or {"kind": "label", "mob_id", "key", "value"}. A grant never matches another mob’s members, and a member on a peer gateway is covered only by a member grant.
  • routes (default one direct route) are {"kind": "direct", "topics"?}, which lets members subscribe to those topics, or {"kind": "triage", "member", "topics"?, "forward_topics", "forward_audience"?}, which hands them to one triage member that may announce on the forward topics, owned by the source, to the forward audience (the audience by default). One member may not hold both a direct subscription and a forward subscription of one source (route_overlap), and a route selects by topic (route_selector_unsupported for streams).
  • conversation_members own a live conversation on the source’s scope and are never its recipients (route_conflict).
  • start is {"kind": "baseline"} (default) or {"kind": "backfill", "max_items"} (at most 1000). delivery is {priority, max_per_batch, on_unhandled}, by default normal, 20 and redeliver. health_policy is {escalate_after_failures, escalate_after_ms, stale_after_ms, quiet_after_ms?, empty_after_ms?}, by default 3, six hours and one day, with the last two off; each window is one minute to 30 days.
  • runner is {"host_runnable", "every_seconds"} (60 to 2,592,000) or absent: the host runnable that collects the source, and its cadence (see Configuration).
An audience or route change is never refused for existing subscriptions: capture, seal and the last check before submission apply it. affected_subscriptions counts the subscriptions it affects, and delivery_readiness reads, for this gateway, whether deliveries can reach members locally, in the shape of meta/capabilities.mob_announcements.local_delivery. A gateway that does not report them leaves them out. A source answers as {mob_id, source_id, generation, revision, identity, connection, observation, topics, audience, routes, conversation_members, start, delivery, health_policy, runner, state, collection, cursor, streams, backfilled, health, created_at_ms, updated_at_ms}. revision rises with every change, across generations. state is active, paused or ended; collection is baselining, backfilling or live. cursor is {version, token, advanced_at_ms}, version 0 until the first step of the generation commits. streams counts {live, onboarding}. health is {status, last_success_at_ms, last_new_item_at_ms, last_observed, last_observed_at_ms, empty_snapshot_since_ms, consecutive_failures, totals, last_failure, last_conflict}, status one of baselining, backfilling, ok, failing, stale, paused or ended. Only sources/get returns the cursor token and a failure’s detail; sources/list shows the principal as {kind, digest}. sources/get with "include_streams": true adds the generation’s streams, [{stream, phase, mode, max_items?, backfilled, since_ms}]. A paused source refuses steps (source_paused) and keeps its cursor; an ended one refuses them (source_ended), and its topics stay reserved to its source_id. A source answer also reads its health as it stands: quiet and empty coverage monitoring, open escalations, and whether this gateway schedules its runner (see Mob announcement health). sources/advance is one collector step, in one transaction: the operation receipt, then the source (unknown_source, source_paused, source_ended), its revision (stale_revision with the current revision, version, generation and cursor), the principal (principal_changed), the cursor version (source_version_conflict with the current version, cursor, state and revision), onboarding, then each item, and last the new cursor, version one higher, even for a step with no items. A refusal changes nothing but the source’s health, which counts the onboarding refusals: read sources/get and start again from the position it reports. An item is {item_id, version, stream?, change: "upsert" | "delete", topics, body, correlation_id?}: topics are the source’s own, a stream is the item’s one container ("" by default), and a delete on a source whose deletions are unobservable is refused deletions_unobservable. Meta derives its key, source: followed by the source id and a digest of the principal, scope, stream, item id, version and change, and a host announce may not use the source: prefix. It announces the item as {"kind": "source", "source_id"}, a new announcer kind that readers of raw announcements may now see; a batch line names the source id, never its connection, principal or cursor. Each item’s outcome is {item_id, version, source_key, outcome, sequence?, captured?, existing_digest?, offered_digest?}: announced (with sequence and captured, a count), replayed (the same content under the key), or conflict (other content under the key, with both content digests: recorded and counted, the original stands, and the step still commits). At most 100 items and 1 MiB of bodies per step, and a cursor token of at most 8 KiB. Onboarding: while a source is baselining a step carries no items (baseline_required) and may span pages, "page": {"index", "final"}; a step without page is a single final page. While backfilling a step announces history, up to start.max_items across its pages (backfill_bound_exceeded with remaining). The final page carries "coverage": "complete" or {"gap": {"reason"}}, and the source turns live when it commits. A step onboards streams with "streams": [{stream, mode: "baseline" | "backfill", max_items?, final}]; an item of a stream that has not started onboarding refuses the step (stream_not_baselined with streams). "rebaseline": {"reason": "cursor_expired" | "reauthorized" | "operator"} turns the source baselining again and announces one gap notice on its topics. observed is {"kind": "delta"} or {"kind": "snapshot", "records"}, what the provider read reported; collected_at_ms is when the collector read it. sources/record_failure takes failure, {"class": "auth_required" | "provider_unavailable" | "rate_limited" | "cursor_invalid" | "adapter_error" | "other", retry_after_ms?, detail?} (detail at most 512 bytes), fenced like a step; the cursor does not move, and the source’s health counts it. audit/list reads a mob’s counted audit entries, {id, at_ms, action, mob_id, member, source_id?, generation?, reason, count, first_at_ms, last_at_ms}, by source, member, action or any of them. One entry counts one source generation, member, action and reason: its count rises and last_at_ms (also at_ms) moves with each repeat. action is subscription_refused, capture_withheld, items_held, items_released, expired_while_held, delivered_across_revocation, redelivered or dismissed; reason is audience_not_covered, no_direct_route, route_overlap, route_conflict or unhandled. An entry for a plain announcement (see Delivery handling) has no source_id or generation. Entries list in id order, the order each was first counted, and after is the id of the last entry read; an entry is kept for the retention window from its last count. meta/capabilities.mob_announcements is null when mob announcements are disabled, and otherwise {"sources": "v1", "local_delivery": {"kind": "ready"} | {"kind": "refused", "cause": "governed_runtime"} | {"kind": "unavailable", "detail"}, "governed_admission": false}: whether deliveries can reach this gateway’s members now. Sources collect on every gateway. The SDKs keep every kind and state above as an open string, and an unknown refusal kind on its code’s error class.

Mob announcement declarations

A host can declare sources and subscriptions instead of putting each one: in its mob announcement options, inline as declarations or in a packaged JSON sidecar file named by declarations_path (see Configuration), or at runtime with declarations/apply. Meta reconciles them at startup, before its dispatcher starts, and at each apply, and creates, replaces and ends only the records a declaration made. Declarations are {revision, sources?, subscriptions?}:
  • revision is the host’s own revision of what it declares; a higher one replaces what is applied.
  • Each of sources takes the sources/put params without operation_id, expected_revision and new_generation, which the reconciler supplies.
  • Each of subscriptions is {subscription_id, member | members, topics, mobs?, start?, delivery?}: member subscribes one member, and members, {"mob_id", "role"}, makes a role template. start is {"kind": "now"}, the only start a declaration takes, and delivery, {"await_handling"?}, is taken as subscriptions/put takes it: absent, or naming nothing, it is the default. A declared subscription_id is at most 200 bytes.
  • mob_id and mobs default to this gateway’s mob, as for sources/put and subscriptions/put.
declarations/apply follows the gateway’s source of declarations: a gateway that declares none, or declares them inline, applies the declarations the call carries, with revision the same as theirs; one with a sidecar takes no declarations in the call and reads the file again. Anything else is refused -32602. A lower revision than the applied one is refused declaration_rollback (with applied), and the applied revision with other declarations declaration_digest_conflict (with revision); the applied revision with the same declarations changes nothing and retries what is pending. Declarations the static checks refuse are refused whole before anything changes, each with its own kind, and at startup they fail the gateway’s init: bounds and shapes, a duplicate source or subscription id (a source id once across every mob), a topic two declared sources own (forward topics too), two declared sources on one principal’s scope (source_identity_taken, naming the first), each source’s configuration as sources/put checks it, a triage member its source’s audience can never cover, a conversation member subscribed to its own scope, and a member whose declared subscriptions together would reach a declared source both directly and through its forward. Each record a declaration makes is marked with the revision and the digest of its declared form. The reconciler creates, replaces and ends only marked records, through the same paths as sources/put, subscriptions/put and the verbs, so every check of those methods applies, and it never touches a record made at runtime. A changed subscription is replaced in place at its revision: what it captured stays owed, and its new selector captures from now. A record dropped from the declarations is ended (a source) or cancelled (a subscription). An operator’s pause holds. An operator’s end or cancel holds while the record’s declaration is unchanged (suppressed); once it changes, a source starts its next generation, and a subscription is pending (subscription_cancelled or subscription_ended) until retention removes the old record, then made again under its id; declared under a new subscription_id it is made at once. A record the runtime refuses for now does not refuse the apply: it is pending with the refusal its put met (such as unknown_member, audience_not_covered or route_overlap), or unmanaged_record when a record made at runtime holds its id, and the rest of the declarations apply. A pending record raises a declaration_pending escalation until it applies, and is retried at startup, at each apply, whenever the gateway’s set of mobs changes and at every health pass. At startup the options’ declarations take effect when nothing is applied yet or they name a higher revision than the applied one, so an apply made at runtime stands across a restart until the options move past it; options at the applied revision with other content are logged once as a warning. A call the reconcile makes that does not answer within 30 seconds (a store call, a read of this gateway’s runtime, or the schedule service) stops it there, as a storage failure does: the apply answers native_unavailable (-32047), or -32000 for a store call, and may already have recorded its revision and made part of it, so retry it with the same revision and declarations; the records it had not reached are finished at the next health pass once the call answers. Until that call ends, a reconcile or a read that would call the same method stops at once. declarations/get, and an apply’s read of its result, answer -32000 when one of their store reads does not answer within 30 seconds or meets one still running. declarations/get waits for a declarations reconcile in progress (at startup, at an apply, after a change in the gateway’s set of mobs, or at a health pass retry) and answers with what it settled. It reads the applied revision (0 before any) and its digest, and records, one per declared source and subscription and one per child of a declared role template: {kind, id, status, refusal?, collection?, delivery?}. A record dropped from the declarations stays listed until its drop takes effect: a drop refused for now, or a dropped source until the schedule service has reconciled its runner after its end. kind is source, subscription or role_child; status is applied, pending, suppressed, or left for a role child whose member left the role; refusal is {kind, message}. An applied source carries collection (baselining, backfilling, live, failing or stale), and an applied subscription or role child carries delivery: {"kind": "ready"}, or {"kind": "refused", "cause"} when deliveries cannot reach this gateway’s members.

Role templates

A subscription with members instead of member is a role template, made through a declaration or subscriptions/put (never the member tools). It never delivers by itself. At each announcement it matches, Meta reads the members the mob’s roster gives the role and captures for each through a child subscription with the id "<template id>/<16 hex digits>", made in the capture’s own transaction, so a member spawned with the role later captures from its first matching announcement. A child passes every check a subscription passes; one that fails is pending with its refusal, captures nothing, and is admitted again at a later capture or trigger. A member that no longer has the role stops capturing at once and keeps what it already owes; its child is left, never cancelled for leaving, and captures again once the member has the role again. A template has at most 256 children (role_template_limit). A subscription and a role template never share an id: either put refuses the id the other holds (subscription_id_taken). A template’s delivery, declared or given to subscriptions/put beside members, is every child’s: a replaced template’s children take the new one with the new selector. subscriptions/put with members answers the template as subscription: {id, members, topics, mobs, state, revision, created_at_ms, updated_at_ms, delivery?}, state active, paused or cancelled, and subscriptions/get with a template’s id answers it as template, beside "subscription": null. A template’s id is at most 200 bytes (-32602 otherwise), so its children’s ids stay within bounds. Replacing its selector admits each child again with the full checks of a put. subscriptions/pause, resume and cancel take a template’s id and change its children with it in one step, answering {template, children, unresolved_batches}: a paused template pauses its live children and makes no new ones, its resume resumes every child its pause paused (a child an operator paused itself stays paused), and its cancel cancels the template and its children as a subscription’s cancel does. Children are subscriptions: subscriptions/get and subscriptions/list read them.

Mob announcement health

A source or member condition that a host should act on is a durable escalation, one open per cause and subject. The dispatcher evaluates every cause each round. It raises an escalation when its condition starts and notifies it again every renotify_after_ms while it stays open (one day by default; see SDK and Gateway runtime options), under the same id with a higher renotify_count. It resolves the escalation when the condition ends. An escalation is {escalation_id, cause, subject, since_ms, raised_at_ms, renotify_count, resolved_at_ms?, evidence?}:
  • since_ms is when its condition began.
  • renotify_count is how many times it was notified again after its raise.
  • subject is {"kind": "source", "mob_id", "source_id"}, {"kind": "member", "member"} or {"kind": "declaration", "record_kind", "id"}.
The causes: Some conditions happen rather than hold: a conflict, a source gap, a batch that reported gaps, or a capture withheld for coverage. Their escalation stays open for a day after the condition last happened. One that happened while no round ran is raised by the next round. Pausing or ending a source resolves its escalations. Every raise, notification and resolution is an event at a monotonic cursor: {cursor, escalation_id, event, cause, subject, since_ms, at_ms, renotify_count}, where event is raised, renotified or resolved. The gateway pushes each event to the host as a meta/on_error notification (see Error hook and logging):
  • mob_announcement_escalated (escalation, event, renotify_count, cursor) for a raise or a notification;
  • mob_announcement_escalation_resolved (escalation_id, cause, subject, resolved_at_ms, cursor) for a resolution.
meta_gateway sends no notifications, so its host reads escalations/list. A host misses the notifications sent while it is away. After a reconnect, escalations/list reads every event after the host’s last cursor. after is that cursor; 0, the default, reads from the first event kept. next is the cursor to read on from. With "open": true, it answers the open escalations instead, and next is the last event cursor. Events and resolved escalations are kept for the retention window. Agents learn of escalations on the reserved topic mob_announcements.health of the subject’s mob, when that mob is hosted here. A member that subscribes to that topic receives each raise and notification when it is covered. A source’s audience covers its escalations, and the health_audience option (none by default) covers member escalations. Resolutions are not announced on the topic, and neither is a declaration escalation, which has no mob. The announcement is a host announcement under a key unique to each escalation and notification, starting with health:. Its body is typed JSON, never adapter text: {event, escalation_id, cause, subject, since_ms, renotify_count, evidence}. A host or member announcement on that topic is refused (-32602), and so is a source that names it as a topic or forward topic. A new subscription to it replays nothing. members/get reads one member’s delivery state. members/list reads a page of members in member order after after: those with an active or paused subscription, a suspend marker or an open escalation. With "escalated": true, it lists only members with an open escalation. Both answer {member, backlog, marker, escalations, handling}:
  • backlog is {eligible_count, eligible_bytes, oldest_captured_at_ms, last_sealed_at_ms, outstanding, held_audience, held_unhandled, held_refused, withheld_audience}:
    • eligible_count and eligible_bytes count the items owed and their bytes;
    • oldest_captured_at_ms is when the oldest was captured, and last_sealed_at_ms when the last batch was sealed;
    • outstanding says whether a batch is in flight;
    • held_audience counts the items held for coverage;
    • held_unhandled counts the items held for an operator after an unhandled turn (see Delivery handling);
    • withheld_audience counts the captures withheld for coverage;
    • held_refused is 0 in this release.
  • marker is the member’s persisted suspend marker or null. It has the shape of a subscription’s marker and keeps the time it was first seen across restarts.
  • escalations lists its open escalations.
  • handling is {pending, handled, handled_unverified, unhandled, held, oldest_unhandled_at_ms}: its admitted batches by handling (handled_unverified is never counted as handled), the items it holds for an operator now, and when its oldest unhandled turn was read (null without one).
A member with nothing recorded answers zero counts, no marker and no escalations. A source answer adds what its health reads now:
  • quiet_monitoring, {configured, quiet_after_ms, last_new_item_at_ms}, and empty_coverage_monitoring, {configured, empty_after_ms, empty_snapshot_since_ms}, say whether quiet and empty coverage detection are configured in its health_policy and what they read.
  • escalations lists its open escalations.
  • status is read when the source is read: failing while a failure stands, in any phase; otherwise stale once its stale window has passed; otherwise its onboarding phase or ok.
  • runner_scheduled_here is null for a source without a runner. Otherwise it says whether this gateway schedules source runners. false means the runner never fires here (see Configuration). The source then collects only when the host advances it, and turns stale otherwise.
With "escalated": true, sources/list lists only sources with an open escalation. It reads past the others until its page is full or the sources end. With mob announcements enabled, meta/status carries a mob_announcements object, or null when the announcement database could not be read: {"sources": {active, paused, failing, stale}, "members": {suspended, unhandled_held}, "handling": {pending, handled, handled_unverified, unhandled}, "escalations": {open, last_event_cursor}}.
  • active counts every active source, and failing and stale the active sources with that health status.
  • suspended counts the members with a suspend marker, and unhandled_held the items held for an operator, over all members.
  • handling counts the admitted batches by handling, over all members.
  • last_event_cursor is where a host that starts now reads escalation events from.

See also