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:
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: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.
-
accepted. After the cheap pre-durable gates pass (mob TOML, model resolution, host-config tables), the gateway answers the request at once:acceptedis not readiness and carries nohttp_base_url. A refusal from those gates is still the plain JSON-RPC error response, and it means nothing past them ran. -
meta/init_progressnotifications, 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, andcleanup(failure cleanup). Meta 0.9.4 no longer reportsprewarm. Progress events are non-terminal, and their order follows the native init order. Callback requests continue to arrive between them. -
Exactly one
meta/init_settlednotification. Only areadysettlement for the sameinit_idestablishes readiness. It carries every field of the legacy init result:Afailedsettlement carries the error that the legacy response would have carried (code,message, optionaldata) plusdurable_effects. The gateway exits after writing it.durable_effectsis"none"only when the init failed before thestoragephase 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 exampleStorageResolutionErrorfor-32014) and adddata.durable_effects.
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.
{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
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 configureTopologyControlPolicy 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.
Parameters
Parameters
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.
Parameters
Parameters
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/retireormeta/retire_member, which needagent.retireon 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_allretires it with the other members that have no registered identity; - is never respawned or reset:
meta/respawn,meta/respawn_memberandmeta/resetrefuse it, on the Console and on the stdio gateway, with-32602,data.kindside_chat_restart_refused(operation,agent_identity,source_member_id), since its successor would be no fork of the agent. An agent’smob_respawntool answers a policy denial with that kind, andmeta/reset_all, where a host declares a baseline member under the side chat’s name (without identity-first authority), lists it underfailedwithkindside_chat_restart_refusedand leaves it as it is. Close it and fork the agent again; - is never adopted as an identity:
mob/adopt_member_identity_declarationrefuses it, on the Console and on the stdio gateway, with-32602,data.kindside_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) andside_chat_of_session(the session it was forked from), and in its experience sidebar row’sside_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_countandimplicit_delegate_idle_retire_secsget side chat values.
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:
-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::reconcileon 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_bindingis 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).
Parameters
Parameters
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’sstate 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 readmeta/status_identityand 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: readmeta/status_identity, and do not repeat the delete. - The delete clears a
brokenstate andwake_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_identityfirst 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.
Console timeline
Console-specific RPC methods are handled by the HTTP console dispatcher and are also listed in the console contractv0.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.
Parameters
Parameters
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).
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:
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.
Parameters
Parameters
{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.
Parameters
Parameters
{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:
- 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 withfailednon-empty and nothing changed. - Raw delegates, helper members with no registered identity, are retired.
- 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.
- The call waits up to 10 seconds for the reset identities’ startup history to reach the console timeline, then returns.
Result
Result
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.
Parameters
Parameters
Response
Response
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.
Parameters
Parameters
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.
Parameters
Parameters
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.
Parameters
Parameters
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.
Parameters
Parameters
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.
Parameters
Parameters
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.
Parameters
Parameters
Result
Result
Routing methods
meta/routing/resolve
Resolve a logical destination to a physical route.
Parameters
Parameters
Response
Response
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.
Parameters
Parameters
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.
Parameters
Parameters
{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.
Parameters
Parameters
{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.
Parameters
Parameters
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.
handle.identity_routing_status(identity) (Python),
handle.identityRoutingStatus(identity) (TypeScript).
Delivery methods
meta/delivery/send
Send a message through a resolved route.
Parameters
Parameters
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 methodsmeta/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’stimeout_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.
Parameters
Parameters
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.
Parameters
Parameters
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.
completedaddsoutput_status,outputandoutput_truncated.output_statusistext(outputis the turn’s own final assistant text from its committed run result, cut at 256 KiB whenoutput_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) orunavailable(the gateway’s session bridge cannot report per-turn output).outputisnullfor every status buttext.failedaddserror. A turn that ran and succeeded is never reportedfailedbecause its session could not be resolved afterwards; that failure is recorded on the identity’slast_delivery_errorinstead.unknownmeans 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.
Parameters
Parameters
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.
Parameters
Parameters
{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, withinput_id,phaseanddurable_witness(trueonly when a committed store row backs it).completed: the delivery’s turn completed, withinput_id,output_status,outputandoutput_truncatedas formeta/turn_result.failed: the turn reached a failed terminal, witherror.terminal_without_run: a terminal disposition without a run of its own, withterminalandlast_run_id.broken: inconsistent evidence, withreason.unresolved: the original owner could not establish the state;causeisruntime_adapter_unavailable,original_session_unknown,original_owner_unavailable(withdetail),evidence_read_timed_outorscope_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 withIdentityRuntime::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_idandcommitted_at_msare the first commit’s, identical on every replay of the key.statusis read for each answer and may advance.status.stateispending(committed, not admitted yet),admitted(withsession_idandinput_id: queued in that session’s input ledger, not proof that a turn ran; follow it withmeta/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) orrefused(final, never admitted, withreason). Tolerate other values.- A
pendingthat survives a gateway restart is not proof of loss: replay the same key, and do not resend the content under a new key.
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.
Parameters
Parameters
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
Themeta/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.
labelsmatches ALL: every listed label must be present on the item (list,ready,snapshot). This is the opposite of the builtintask_listtool, which matches any label.include_terminaldefaults tofalseand drops completed, cancelled, and failed items even whenstatusesnames one of them;statuses: ["completed"]withoutinclude_terminal: truereturns nothing. The bundled console panel passesinclude_terminal: true, so it can show items the SDK default hides.limitdefaults to 100 and may not exceed 1000 (-32602beyond it).- Nothing deletes items or edges. Close an item as
cancelledorfailed, or link a replacement with kindsupersedes;attention/pruneis the one deleting verb and removes only terminal attention-binding rows. The event stream keeps the history. - A
parentedge points from the child (from_id) to the parent (to_id); useblocksonly when the target must not be ready until the source is terminally resolved. closewithoutstatusrecordscompleted. Passstatusexplicitly 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 persistentrpc_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:indexis the row’s 0-based message index (the index space of the history read), andcontent_sha256issha256:<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(defaultfalse): 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 therevisiona 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 alwayssystem-row-prune; there is no caller-supplied reason.
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:
-32602withdata {kind: "system_row_prune_refused", index, cause}: the row atindexcannot be removed, and nothing was written.causeisout_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) orbefore_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).SystemRowPruneRefusedErrorin both SDKs.-32602withdata {kind: "transcript_revision_conflict", expected, actual}:expected_parent_revisionis not the session’s current revisionactual, because a turn ran or another edit committed since the read; nothing was written. Read the history again.TranscriptRevisionConflictErrorin both SDKs.-32015: the member is running a turn or holds an active runtime admission. Retry when it is idle.-32603with 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.IdentityWakeHeldErrorin both SDKs.-32047candidate_launchon a candidate launch.-32016when 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 formeta/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, withdata.instruction_activation_code:session_busy,live_channel_open,target_not_materialized,external_write_fence_conflictandexternal_write_fence_backoffanswer-32015(nothing was appended; retry at your own next boundary);unsupported_current_loweringanswers-32602;durability_unavailableanswers-32603.session, withdata.session_error_code(Meerkat’s stable session error code). When the session owner refused the request itself,data.instruction_activation_error_codecarries Meerkat’s typed code and the error is-32602:invalid_request,digest_mismatch,effective_activation_conflict(a staleexpectation; re-readkey_state),activation_identity_conflict,immutable_revision_conflict,recorded_but_not_effective, orinherited_activation_requires_explicit_activation.malformed_activation_historyanswers-32603. A busy session answers-32015; any other session error answers-32603.runtimeandowner_task: the runtime authority or the owning task failed.
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).
Result
Result
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).
Parameters
Parameters
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.
Result
Result
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 removedmeta/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.
broken_members entries
broken_members entries
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.
quarantined_mobs entries
quarantined_mobs entries
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.
reserved_identity_members entries
reserved_identity_members entries
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.
non_expiring_console_principals entries
non_expiring_console_principals entries
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.
Parameters
Parameters
Result
Result
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.
Parameters
Parameters
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.
Result
Result
Console ingress
The olderconsole.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).
redeliveris 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) isunhandledeven 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.redeliveredcounts these items. An item is redelivered automatically at most twice.holdkeeps the items for an operator, andheldcounts 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 asunhandled(see Mob announcement health).reportis the default for plain announcements, as in 0.9.4. It only records and counts the outcome.
deliveries/redeliverchecks 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/dismisssettles the held items as handled by an operator. They are never delivered again.
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 andsource_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:
identityis{provider, principal, scope: {kind, id}}and is fixed within a generation (identity_immutable).principalis 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_allowedotherwise). Two sources of one mob cannot share a provider, principal and scope (source_identity_taken).connectionis{"kind": "connect_setup" | "host", "id"}, a reference Meta keeps and never dereferences.observationis{"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_ownedfor another source’s, and a plain announce on one is refusedtopic_owned_by_source).audiencelists 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_unsupportedforstreams).conversation_membersown a live conversation on the source’s scope and are never its recipients (route_conflict).startis{"kind": "baseline"}(default) or{"kind": "backfill", "max_items"}(at most 1000).deliveryis{priority, max_per_batch, on_unhandled}, by defaultnormal, 20 andredeliver.health_policyis{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.runneris{"host_runnable", "every_seconds"}(60 to 2,592,000) or absent: the host runnable that collects the source, and its cadence (see Configuration).
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 asdeclarations 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?}:
revisionis the host’s own revision of what it declares; a higher one replaces what is applied.- Each of
sourcestakes thesources/putparams withoutoperation_id,expected_revisionandnew_generation, which the reconciler supplies. - Each of
subscriptionsis{subscription_id, member | members, topics, mobs?, start?, delivery?}:membersubscribes one member, andmembers,{"mob_id", "role"}, makes a role template.startis{"kind": "now"}, the only start a declaration takes, anddelivery,{"await_handling"?}, is taken assubscriptions/puttakes it: absent, or naming nothing, it is the default. A declaredsubscription_idis at most 200 bytes. mob_idandmobsdefault to this gateway’s mob, as forsources/putandsubscriptions/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 withmembers 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 everyrenotify_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_msis when its condition began.renotify_countis how many times it was notified again after its raise.subjectis{"kind": "source", "mob_id", "source_id"},{"kind": "member", "member"}or{"kind": "declaration", "record_kind", "id"}.
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}:
backlogis{eligible_count, eligible_bytes, oldest_captured_at_ms, last_sealed_at_ms, outstanding, held_audience, held_unhandled, held_refused, withheld_audience}:eligible_countandeligible_bytescount the items owed and their bytes;oldest_captured_at_msis when the oldest was captured, andlast_sealed_at_mswhen the last batch was sealed;outstandingsays whether a batch is in flight;held_audiencecounts the items held for coverage;held_unhandledcounts the items held for an operator after an unhandled turn (see Delivery handling);withheld_audiencecounts the captures withheld for coverage;held_refusedis 0 in this release.
markeris the member’s persisted suspend marker ornull. It has the shape of a subscription’smarkerand keeps the time it was first seen across restarts.escalationslists its open escalations.handlingis{pending, handled, handled_unverified, unhandled, held, oldest_unhandled_at_ms}: its admitted batches by handling (handled_unverifiedis never counted ashandled), the items it holds for an operator now, and when its oldest unhandled turn was read (nullwithout one).
quiet_monitoring,{configured, quiet_after_ms, last_new_item_at_ms}, andempty_coverage_monitoring,{configured, empty_after_ms, empty_snapshot_since_ms}, say whether quiet and empty coverage detection are configured in itshealth_policyand what they read.escalationslists its open escalations.statusis read when the source is read:failingwhile a failure stands, in any phase; otherwisestaleonce its stale window has passed; otherwise its onboarding phase orok.runner_scheduled_hereisnullfor a source without arunner. Otherwise it says whether this gateway schedules source runners.falsemeans the runner never fires here (see Configuration). The source then collects only when the host advances it, and turnsstaleotherwise.
"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}}.
activecounts every active source, andfailingandstalethe active sources with that health status.suspendedcounts the members with a suspend marker, andunhandled_heldthe items held for an operator, over all members.handlingcounts the admitted batches by handling, over all members.last_event_cursoris where a host that starts now reads escalation events from.
