Skip to content

Maia normative contracts

Status: planned target contract. Each section names what exists in source today (verified in packages/protocol/src, packages/agent/src) and the roadmap slice that delivers the rest. Nothing here makes an operation callable; only the registry and a complete vertical slice do.

Authority

  1. packages/protocol owns the executable Effect schemas and the operation registry. Today the registry is the method list in packages/protocol/src/local-node/node-api/methods.ts plus feature descriptors (maia.feature-registry/v1, packages/protocol/src/feature/) that carry risk, idempotency, confirmation, receipt and projection metadata.
  2. This document owns cross-domain invariants, schema requirements, versioning and generation rules.
  3. cli.md owns human command grammar and example output.
  4. Domain architecture documents own algorithms and lifecycle policy. They may narrow these contracts but never redefine them.
  5. Generated help, JSON Schema, SDK types and OpenAPI are projections and never override the registry.

Only this repository is authority. Historical Maia OS repositories, Pi Config, Codex and Pi CLI documents are migration evidence.

Version and compatibility policy

Today: maia.protocol/v1alpha1 (MAIA_PROTOCOL_VERSION), plus per-domain schema versions such as maia.execution/v1alpha1, maia.terminal-session/v1alpha1 and command receipt schemaVersion: 1.

During alpha a breaking change is allowed only when all of these change in one change: the registry digest; the affected Effect schemas and fixtures; the CLI, SDK, Node, worker and documentation projections; a migration or explicit refusal for persisted data; the compatibility matrix and release notes.

  • Every connection starts with the node.hello handshake (RL2). Release owns its fields, the skew policy and protocol_incompatible refusal (decision).
  • An operation descriptor has since, stability and an optional replacementOperationId. A renamed or removed operation, command, flag or config key keeps an alias with machine-readable deprecation diagnostics for at least 90 days and at least one minor release during alpha (two after v1 stable), recorded with a removeAfter date that CI enforces.
  • CI compares the operation registry with the last released snapshot. An addition needs a minor bump. During alpha a breaking change follows the rule above and raises the minor; after 1.0, removals and incompatible changes are refused within a major.
  • Unknown fields fail in security-, identity-, authority- and mutation-bearing objects. A read projection may ignore additive fields only when its schema marks them forward-compatible.
  • storage.integrity of node.status and storageIntegrity of the voice status now accept unavailable next to ok. An older client cannot decode unavailable, so client and Node upgrade together. ok means the storage connection is open and, when the Node reports retention, its journal counters read; it does not mean an integrity check passed.
  • storage.events of node.status is optional (excessBytes, oldestAgeMs, checkpointBlocked; see events). A client built with it accepts an older Node. A client built before it rejects a newer Node's response, because unknown fields fail: during alpha, client and Node upgrade together.
  • Source additions to v1alpha1 that a reader built before them rejects, because unknown fields fail: the session.attach stream records (maia.session-stream/v1, SESSION_STREAM_SCHEMA_VERSION); the held error code (HOLD_ADMISSION_ERROR_CODE) and the holdId of session.stopped; the Session credential scheme (NODE_API_SESSION_CREDENTIAL_SCHEME) with SESSION_ROLE_OPERATIONS and SESSION_GUARD_ERROR_CODES, where an older Node refuses the Session-authenticated session.list; the optional cwd of session.create (a start directory that an older Node rejects as an unknown field), the optional hostAuthority and permission of session.create with the containment of a Session and the hostAuthority of node.status (D-198; an older Node rejects the fields, so a client never falls back to the sandbox) and HOST_AUTHORITY_DISABLED_ERROR_CODE, the optional labels of session.create and a Session (also rejected by an older Node; MAX_SESSION_LABELS), the optional labels, settled and idleForSeconds filters of session.list (with settledReason on its items), the optional summary and transcriptPath of session.archive and its result (MAX_SESSION_ARCHIVE_SUMMARY_BYTES, MAX_SESSION_TRANSCRIPT_PATH_BYTES, DEFAULT_SESSION_SETTLED_IDLE_SECONDS, MAX_SESSION_SETTLED_IDLE_SECONDS), the optional role, labels and title of session.adopt.preview (echoed in its result and bound into its digest) and MANAGER_EXISTS_ERROR_CODE (D-201), and Session titles (MAX_SESSION_TITLE_LENGTH) with session.rename, session.renamed and reference.resolve (MIN_REFERENCE_PREFIX_LENGTH, MAX_REFERENCE_CANDIDATES); and the machine kind of the context.show principal with its machine block (MachinePrincipal, maia.invocation-context/v1 unchanged because no Node emits it before MN1 enrollment: an older reader refuses a machine projection, which no deployed Node sends). The node.status process-resource values cgroup_delegation_unavailable (pressure) and run_node_in_delegated_cgroup (nextStep), and landlock_scoped_unavailable (pressure) with upgrade_kernel (nextStep), are such additions: a Linux Node outside a delegated cgroup, or on a kernel below Landlock ABI 6 (or whose probe failed), sends them, and an older reader refuses that status instead of showing hard containment that every launch would refuse. The executorHost field of a Linux Node's node.status and the session.create error codes in EXECUTOR_HOST_ERROR_CODES (host_bubblewrap_missing, host_socat_missing, host_user_namespaces_restricted) are additions too: an older reader refuses a Linux Node's status, and still reads the refusals, because an error code is any well-formed string. An error that carries a key its reader does not know is rejected as invalid, and a bridge failure frame then keeps only the bridge's own code. The bridge request frame's principal (coordinator) is another: an older bridge closes on a marked frame as invalid. A newer bridge forwards the mark as x-maia-principal, which every published Node enforces; a Node built before that header ignores it, so the bridge and its Node run from the same install. The bridge ready frame gains no field, so an older CLI still reads a newer bridge's greeting. A bridge built before streams refuses a subscribe frame as invalid and exits 65; the SDK maps that exit, after a subscribe with no item or end, to command_not_implemented. A newer bridge ends a subscribe for any other operation failure stream_unsupported, and a subscribe marked principal: "coordinator" failure role_denied, the code the Node's gate uses. Prerelease versions ship the CLI and Node together.
  • Mixed-version order: deploy a backward-readable Node; migrate durable schemas; deploy worker and SDK/CLI projections; enable new operations; remove the old reader only after restore and downgrade-refusal tests.

Identifier and reference grammar

Today the wire accepts protocol identifiers of 1–128 characters matching ^[A-Za-z0-9][A-Za-z0-9._:-]*$, and the Node generates <prefix>_<32 hex> IDs (container_, project_, receipt_, ...).

Target: identifiers are opaque ASCII with a type prefix and 128 bits of random or deterministic idempotent entropy, matching ^[a-z][a-z0-9-]{1,31}_[A-Za-z0-9_-]{16,128}$. Prefixes include project_, session_, activation_, attempt_, message_, routine_, tick_, node_, attachment_, connection_, effect_, event_, receipt_, artifact_ and evaluation_.

On the CLI, a resource argument may also be a unique ID prefix (at least four hexadecimal digits) or a unique name or Session title; the Node's reference.resolve turns it into the exact ID before any operation, and an ambiguous one fails listing the candidates (CLI). Operations themselves take exact IDs only.

Human definition references are never bare IDs:

builtin:<id>
user:<id>
project:<project-id>/<id>

Agent, Skill, Routine, evaluator, result-contract, Connection and executor references use this grammar. A Project-local CLI command may accept a short ID only after an exact --project resolves the namespace; JSON always uses the full reference.

  • Revisions are positive safe integers. A mutation names every aggregate revision it depends on.
  • Timestamps are UTC RFC 3339 with millisecond precision. Durations are integer milliseconds.
  • Digests are lowercase SHA-256 with a sha256: prefix, over canonical UTF-8 JSON or exact resource bytes.

Workspace and Execution Environment naming

Approved target vocabulary, not yet API names (decision). Roadmap R1b schedules it; PF-25/DF-25 accept it. Current commands, SDK bindings, schema types, fields, owner discriminants and IDs keep their meaning until then.

  • Workspace means only the organizational parent of Projects. A Project owns focused work, knowledge and workers.
  • Execution Environment is a checkout, Git worktree or separately admitted private working directory; Execution Node is the host computer or server; Execution Profile is the permission and containment policy. These are execution resources, not organizational levels. A working directory or worktree is not a sandbox.
MeaningCurrent compatibility contractFuture versioned contract
Organizational Workspacecontainer.*; containerId; existing container_* IDsworkspace.*; organizational workspaceId
Execution Environmentworkspace.*; execution workspaceId; existing workspace_* IDsenvironment.*; environmentId

New product vocabulary uses Workspace and Execution Environment, never Container, Execution Workspace or a second ProjectEnvironment concept. Source references may keep the old names when labelled.

Migration requirements, under the version policy:

  • Freeze the exact old/new operation, field, owner and identity mappings in the registry. Bind dispatch to the negotiated version and descriptor digest; never infer organizational or execution meaning from a display name, prefix, UI focus or alias. An unversioned workspace.* request never silently changes target kind; unsupported clients fail before effects.
  • Preserve organizational and execution IDs, Project memberships, repositories, filesystem identities, Session and assignment references, grants, history, events and receipts. Never rewrite ID prefixes or reinterpret execution rows as organizations. Retained IDs get version-aware validation; new-ID allocation and any storage migration or refusal are explicit decisions. One Node-owned writer.
  • Stored confirmations and receipts keep their exact operation, input and revision binding. Retries reconcile under their original contract and idempotency key; a pending proof never becomes authority in the new namespace. If an old contract cannot resume safely, return an explicit refusal with a receipt-reconciliation path.
  • Deliver protocol → SDK → Node → CLI → applicable agent, voice and client projections with mechanical parity and mixed-version tests. Expose new names only after their registry entries and handlers qualify; keep old help labels marked as compatibility names.
  • Test empty and populated disposable databases, identity and history preservation, concurrent old and new readers and commands, stale or expired proofs, duplicate and lost-response retries, wrong-kind IDs, unsupported-version refusal, rollback, restore and downgrade refusal. Freeze backup and recovery before any separately authorized live migration.

Ordinary Workspace Session ownership

Required for R3 (PF-03/PF-22, DF-22); not current runtime support. A Workspace can own several ordinary Sessions, even with zero Projects. Creating, listing, inspecting and reopening them needs no Project, repository, checkout or Execution Environment and provisions no coordinator. A Workspace coordinator (home) Session is optional and separate; disabling it never disables ordinary Sessions.

  • One owner. Every Session has exactly one closed global, Workspace or Project owner, independent of its coordinator or ordinary role. A Workspace-owned Session binds the exact organizational Workspace identity, not a Project ID or a legacy execution workspaceId. Ownership is never inferred from navigation, a role name, a parent Session or a path; conflicting or forged owner combinations are rejected.
  • Distinct Sessions. Several ordinary Sessions with the same owner and Agent stay distinct; home-Session uniqueness applies only to the designated home Session. Separate create commands make separate Sessions; an exact retry reconciles its original command and receipt.
  • Scoped access. Grants, history and activation context bind the authenticated principal, the exact owner and the Session. Workspace membership does not authorize reading another conversation or reusing its provider history, private context, tool grants or caches. Shared Workspace knowledge comes through explicit grants and the admitted learning policy.
  • Durable identity. Identity, owner, messages, revisions, receipts and history survive disconnect and restart. Reopening history is a read, not model activation or archive restore. Navigation, adding a Project or delegating work never reparents a conversation; Project execution uses a separately authorized Project-owned Session through parent-mediated delegation.
  • No implicit execution. Creating Session metadata starts no worker and grants no filesystem, process, network or provider access. Conversation-only activation needs its own owner-aware configuration, context, provider, export and budget grants. No Project configuration, ambient working directory, synthetic Project or hidden environment may stand in for them; private Node storage is not a worker filesystem grant.
  • One backend. Extend the existing Session owner through protocol → SDK → Node → built CLI → applicable projections, not a second chat backend. Keep old Project Session IDs, bindings, history, events and receipts, and coordinate field mappings with the naming contract. Unsupported owner and operation combinations fail closed.

Acceptance includes an empty Workspace with two ordinary Sessions and no coordinator. This section reserves no operation name, live migration, provider call or execution profile.

Coordinator ownership and evaluation extensions

Required target extensions (R1, R3, R10), not current schemas. No operation ID or tool is reserved here.

  • Owners. Protocol owns the closed global, Workspace and Project owner variants and the authenticated Session and principal binding. A Workspace keeps its container_* identity until R1b. Existing Project IDs, histories and receipts stay readable through a preserving migration or an explicit refusal; there is no all-powerful synthetic operator Project.
  • Visibility. The Swarm ownership contract owns principals, discovery ceilings and parent-mediated delegation. Effective access is the intersection of owner ceiling, grants and operation policy, applied before any catalog, search, count, traversal, error or model export.
  • Semantic evaluation. The evaluation contract needs one registry slice with typed answers, uncertainty, errors, owner/export/budget admission and receipt reconciliation, shared by internal services, SDK, CLI and granted coordinator tools. Execution workers get no direct evaluation access.
  • Graph snapshots extend the observation contracts with entities, relationships, source revisions, per-owner watermarks and coverage. Semantic annotations stay advisory; no model creates ownership, dependency or lifecycle facts. Qualification: PF-22–PF-24 and DF-22–DF-24 in the portfolio suites.

Routing, context and feedback contracts

Planned (R10), not registered operations. Swarm owns routing and attention policy, Node owns journal and context state, and presentation owns device signals. Freeze versioned, closed Effect Schemas and parity tests for:

  • Input origin lanes (human request, coordinator task request, worker event, feedback) with server-authenticated provenance; causality never creates human authority.
  • Bounded plans over existing operations: dependencies, shared constraints, acceptance and cumulative limits, validated like any mutation (grants, inputs, revisions, expiry, digest, idempotency, confirmation).
  • Work and feedback queues with real received, queued, handoff, running and result evidence, unknown outcomes, source sequences, restart behavior, finite quotas and typed overload. A receipt is not proof of execution or attention.
  • Session graph facts with separately labelled annotations, coverage, context manifests and retained constraints; a central model and effort policy frozen before model use.
  • Client focus snapshots bound to the exact device, view, Session, selection generation, allowed signals, age and TTL. No global focus or silent retargeting.
  • Advisory scores and feedback decisions with rubric, model and source identity, freshness, consent and the originating request. Required visibility and replies stay; no score grants authority.
  • AI readiness states (invalid, stale, denied, over-budget, unavailable). A required model or Jev failure blocks only the affected automation, never manual controls; retries never invent another paid attempt or provider.

SDK methods return validated typed objects; Node serializes them as JSON; the CLI projects human output or --json/--jsonl. UI clients consume SDK results, never CLI output or raw worker logs. PF/DF-29 covers the composed behavior.

Client presentation contracts

User-approved R9a/R10 target, not registered operations (decision). Node owns versioned, bounded, json-render-compatible presentation output through the public SDK. Presentation runtime owns product composition, the view-to-SDK map, failure isolation and multi-client limits; this section owns the protocol admission rules. A catalog is presentation vocabulary, not a second registry or authorization source.

Freeze exact request, result, error and event schemas and SDK bindings first:

  • Binding. Authenticate user, device and client server-side and bind Node identity and epoch, exact owner and target set, Session, view and request identity, catalog version and digest, policy revision and selection generation. Caller identity claims, global focus and client capability declarations are not grants. Global Maia is user-level, not a synthetic Project.
  • Bounded streams. One shared schema and catalog serves separate client view instances. Specs and patches are bounded by bytes, nesting, component and reference count, opcodes, paths, sequence and base revision, with explicit cancellation, end, error, version refusal, gap and resnapshot, and catalog negotiation. Unsupported or incomplete specs never partially enable actions. No UI eval, dynamic imports, arbitrary URLs or hidden on-render operations.
  • Authoritative data stays separate, as the presentation runtime's spec, data and action boundaries define; a patch is never a domain mutation.
  • Trusted components. Shell, target, confirmation and receipt components are authored with protected inputs and placement; generated specs cannot supply their proofs or imitate them. UI actions map mechanically to admitted operation IDs and go through ordinary validation, confirmation, compare-and-set, idempotency and receipts.
  • Policy before work. Owner, export and retention policy apply before Jev, cache lookup, query, count, graph traversal and output. Cache equivalence includes the authorized audience, policy and source revisions, request or rubric digest and catalog digest. Revocation invalidates queued work; one subscriber's cancel never cancels another's shared computation.
  • Limits follow the multi-client admission contract. UI disconnect releases UI resources, not accepted work; unknown mutation outcomes reconcile the original command.
  • Effect Schema is canonical. Qualify renderer-catalog compatibility without handwritten duplicate validators; json-render fetch hooks or generation endpoints never become another request boundary.

Current envelopes, schemas and container.*/execution workspace.* meanings are unchanged. R9a freezes the delta in protocol → SDK → Node → headless fixtures → projections. PF-27/DF-27 and PF-28/DF-28 require parity, rejection of malformed, stale and foreign-client input, voice and target races, fault isolation and measured multi-client fairness.

Configuration contracts

Today the protocol defines one maia.config/v1alpha1 document (packages/protocol/src/configuration), named by config.jsonc and profiles/<name>.jsonc (no Node code reads them today; S13 owns the configurator), and the agent home reads ~/.maia/config.toml (schema_version = 1). The V1 shapes below are the target. Architecture owns the .maia scopes, file layout and admission flow.

~/.maia/config.jsonc parses into UserConfigV1:

interface UserConfigV1 {
  readonly schemaVersion: "maia.user-config.v1";
  readonly swarm: {
    readonly administratorAgentRef: `user:${string}`;
    readonly defaultTimezone: string;
    readonly capacity: CapacityLimitsV1;
  };
  readonly defaults: {
    readonly connectionRef?: `user:${string}`;
    readonly modelRef?: string;
  };
  readonly observability?: ObservabilityPolicyV1;
}

~/.maia/projects.jsonc parses into UserProjectInventoryV1, a list of:

interface ProjectInventoryEntryV1 {
  readonly projectId: string; // opaque, Node-owned
  readonly projectKey: string; // portable human key
  readonly rootPath: string; // canonical absolute root
  readonly workspaceKind: "repository" | "directory";
  readonly repository?: {
    readonly normalizedRemote: string;
    readonly objectFormat: "sha1" | "sha256";
    readonly expectedHead?: string;
  };
}

The canonical root is resolved descriptor-first without following a final symlink; parent and file must belong to the current OS user and not be group or world writable. Duplicate canonical roots, or duplicate repository identity plus ref, are rejected. A move or reclone is a relocation mutation; editing the file never relocates a live Project.

<root>/.maia/config.jsonc parses into ProjectConfigV1:

interface ProjectConfigV1 {
  readonly schemaVersion: "maia.project-config.v1";
  readonly projectKey: string;
  readonly defaults?: {
    readonly agentRef?: string;
    readonly connectionRef?: string;
    readonly modelRef?: string;
    readonly executorRef?: string;
  };
  readonly limits: CapacityLimitsV1;
  readonly context?: {
    readonly nestedAgentsMode?: "disabled" | "nearest-explicit";
    readonly maxInstructionTokens?: number;
    readonly maxSkillIndexTokens?: number;
  };
  readonly allowedConnections?: ReadonlyArray<string>;
  readonly allowedExecutors?: ReadonlyArray<string>;
  readonly maintenance?: {
    readonly enabled: boolean;
    readonly autoApplySafeChanges: boolean;
  };
  readonly retention?: RetentionPolicyV1;
}

interface CapacityLimitsV1 {
  readonly maxLiveSessions: number;
  readonly maxConcurrentActivations: number;
  readonly maxChildrenPerSession: number;
  readonly maxAgentDepth: number;
  readonly maxPendingRoutineTicks: number;
  readonly maxQueuedMessages: number;
  readonly maxInputTokensPerActivation: number;
  readonly maxOutputTokensPerActivation: number;
  readonly maxWallTimeMsPerActivation: number;
  readonly maxOutputBytesPerTool: number;
  readonly maxDailyCostMicros?: number;
  readonly currency?: "USD" | "EUR";
}

projectKey matches ^[a-z][a-z0-9-]{1,63}$ and must equal the admitted inventory entry; it is metadata, not authority or a global ID. Project files may write project:self/<id> for a definition in the same Project; admission resolves it once to project:<opaque-project-id>/<id>, and project:self is invalid in API, event, receipt, trace or provider JSON. CapacityLimitsV1 closes every admission dimension.

Resolution and admission

Resolution order is fixed, with no implicit shadowing:

  1. reviewed built-ins;
  2. admitted user definitions the Project references explicitly;
  3. admitted Project definitions;
  4. the Agent definition;
  5. activation-time narrowing.

Duplicate fully qualified references are errors, and Project definitions cannot redefine builtin: or user: resources. ResolvedProjectSnapshotV1 records every transitive resource with its source scope, canonical locator, content digest, schema version, trust class and capability-evidence digest; missing or stale evidence makes the candidate inadmissible.

Admission validates the candidate from opened file descriptors, builds one canonical manifest, previews the exact diff, and commits the snapshot, Project revision, ordered event and receipt in one transaction. Managed activations use the admitted snapshot ID. Local Agent mode uses the same compiler in memory and reports admission: "local-unmanaged".

Operation registry

Every public read or mutation is one OperationDescriptorV1 (target; today's feature descriptors carry a subset):

interface OperationDescriptorV1 {
  readonly operationId: string;
  readonly since: string;
  readonly stability: "alpha" | "beta" | "stable" | "deprecated";
  readonly replacementOperationId?: string;
  readonly owner: "protocol" | "node" | "swarm" | "provider";
  readonly availability: ReadonlyArray<
    "cli" | "tui" | "sdk" | "model" | "voice" | "a2a"
  >;
  readonly inputSchemaRef: string;
  readonly outputSchemaRef: string;
  readonly errorSchemaRef: string;
  readonly risk: "read" | "reversible" | "consequential" | "destructive";
  readonly authorizationScope: string;
  readonly revisionFields: ReadonlyArray<string>;
  readonly idempotency: "none" | "optional" | "required";
  readonly confirmation: "none" | "required";
  readonly effectClass:
    "none" | "native-idempotent" | "native-reconcilable" | "external-uncertain";
  readonly eventTypes: ReadonlyArray<string>;
  readonly receipt: boolean;
  readonly stream?: {
    readonly mode: "snapshot" | "events" | "snapshot-then-events";
    readonly cursorSchemaRef: string;
    readonly resetSchemaRef: string;
  };
  readonly redactionPolicyRef: string;
}

The registry rejects duplicate operation IDs, semantic aliases with overlapping mutation ownership, missing schemas, model or voice exposure without an explicit policy, mutations without receipts, and consequential operations without idempotency and confirmation.

Package entry points

Today's operations carry a stable ID such as session.create, turn.wait or receipt.get, strict parameter and result schemas, an Agent exposure class (eligible, human-only or unsupported) and receipt rules. A new operation must be classified to compile. Every schema exposes parse and safeParse, with its TypeScript type inferred from it; Effect types do not leak into the public API.

@maiaos/protocol exports these entries (packages/protocol/package.json). A generated public operation catalog and device-capability schemas are planned, not exported.

Entry pointContents
@maiaos/protocolOperation registry, envelopes, errors, receipts, domain schemas
@maiaos/protocol/agent-definitionAgent definition schema, canonical digest encoding, eligibility
@maiaos/protocol/node-profileNode.js-only profile validation and host paths
@maiaos/protocol/feature-registryStatic registry binding features to SDK, CLI and Node handlers
@maiaos/protocol/voice-orchestratorVoice orchestrator prompt and tool contracts
@maiaos/protocol/voice-pi-relayVoice-to-Pi relay preview, relay and reconcile contracts
@maiaos/protocol/execution-candidateExecution, worktree and checkpoint candidate contracts
@maiaos/protocol/invocation-context-candidateInvocation context candidate contracts

Capability enablement

One durable registry object controls risk-bearing adapters and execution profiles:

interface CapabilityEnablementV1 {
  readonly enablementId: string;
  readonly capabilityId: string;
  readonly ownerActorId: string;
  readonly scopeRefs: ReadonlyArray<string>;
  readonly adapterDigest: string | null;
  readonly schemaDigest: string;
  readonly policyDigest: string;
  readonly evidenceRefs: ReadonlyArray<string>;
  readonly reason: string;
  readonly status: "enabled" | "revoked" | "expired";
  readonly expiresAt: string;
  readonly revision: number;
  readonly createdAt: string;
  readonly updatedAt: string;
}

scopeRefs holds 1–32 exact typed actor, Project, Connection, peer, Node or execution-profile references; empty or wildcard scope is invalid. The registry owns the closed capability IDs and their required evidence. Only a human local administrator may enable a capability; a narrower authorized owner may revoke it. Expiry and revocation are monotonic; re-enabling creates a new record.

Mutation, preview, confirmation, and receipt

Today each consequential operation has its own preview and confirm methods (for example container.create.preview and container.create.confirm): the preview returns an inputDigest, a proof, issuedAt and expiresAt; the confirm repeats the input with the digest and proof and returns delivery (new or duplicate) and a CommandReceipt (schemaVersion: 1; packages/protocol/src/command-receipt), retrievable with receipt.get and receipt.list. receipt.list filters on the methods that record a receipt under their own method, listed in packages/protocol/src/local-node/receipt-list.ts: a described operation is listed exactly when its policy.ownReceipt is true, which the type checker enforces, and the other entries are a reviewed inventory of undescribed methods. Returning or reconciling another operation's receipt (system setup, voice managed confirmation) does not make a method filterable. The generic contract below is the target.

The pruned-history markers are optional and absent unless a prune left a hole (D-191): oldestRetainedCursor on the session event list result and on the terminal event list result of an unavailable terminal, and eventsPruned: true on receipt.get. A live terminal keeps its own 50,000-event history and reports an evicted range in the list itself as a gap event naming the oldest cursor still held. Wire decoding rejects unknown fields, so a client built before these fields fails on a page that has a hole. That is an alpha paired-upgrade break, first reachable when the pruner is enabled.

operation.preview takes an exact operation ID and canonical candidate input, authorizes the actor, resolves targets, validates revisions and returns:

interface OperationPreviewV1 {
  readonly previewId: string;
  readonly operationId: string;
  readonly normalizedInputDigest: `sha256:${string}`;
  readonly actorId: string;
  readonly deviceId: string;
  readonly targets: ReadonlyArray<{
    readonly targetType: string;
    readonly targetId: string;
    readonly expectedRevision: number;
  }>;
  readonly risk: "reversible" | "consequential" | "destructive";
  readonly summary: string;
  readonly expiresAt: string;
  readonly previewRevision: number;
}
  • confirmation.issue is allowed only to an authenticated human client. It binds the preview, actor, device, exact digest, targets, revisions and expiry. confirmation.consume and the target mutation run in the same Node transaction. Speech and model tools may request a preview but never issue or consume a confirmation.
  • Every mutation retry reuses commandId. The uniqueness key is actorId + operationId + commandId; a different normalized input under the same key returns idempotency_conflict. Keys and receipts live at least as long as the affected aggregate and never less than 30 days.
interface OperationReceiptV1 {
  readonly schemaVersion: "maia.receipt.v1";
  readonly receiptId: string;
  readonly commandId: string;
  readonly operationId: string;
  readonly actorId: string;
  readonly projectId?: string;
  readonly normalizedInputDigest: `sha256:${string}`;
  readonly outcome: "applied" | "rejected" | "reconciled" | "outcome-unknown";
  readonly targets: ReadonlyArray<{
    readonly targetType: string;
    readonly targetId: string;
    readonly priorRevision?: number;
    readonly resultRevision?: number;
    readonly disposition: string;
  }>;
  readonly eventIds: ReadonlyArray<string>;
  readonly effectIds: ReadonlyArray<string>;
  readonly evidenceDigest: `sha256:${string}`;
  readonly createdAt: string;
}

First-owner bootstrap operations

Source candidate. Two release-disabled operations (releaseQualified: false, CLI-only projection) in the team-workspace feature (packages/protocol/src/team-workspace), specified in Team Workspaces and control Nodes:

OperationRiskConfirmationReceiptSDK method
team-workspace.bootstrap.previewreversiblenonenonepreviewTeamWorkspaceBootstrap
team-workspace.bootstrap.applyconsequentialtrusted-client-requiredrequiredapplyTeamWorkspaceBootstrap
  • Both take a commandId and expectedBootstrapRevision, and reject unknown fields and any caller-supplied ID, role, authority, credential or timestamp.
  • The preview result carries inputDigest, previewId and the reserved IDs but no proof. The confirmation is a one-shot record on a private inherited channel from the launcher to the Node. It is in no request, result, SDK type or CLI flag, and a bearer alone cannot supply it.
  • The apply receipt names method team-workspace.bootstrap.apply, target type control-node-bootstrap and aggregate revision 1. receipt.list filters on it because the method records its own receipt. receipt.get(commandId) returns the stored result that rebuilds the apply result.
  • The closed error set is bootstrap_already_completed, bootstrap_authority_unavailable, bootstrap_binding_mismatch, bootstrap_channel_invalid, bootstrap_confirmation_required, bootstrap_integrity_failed, bootstrap_preview_expired, idempotency_conflict and revision_conflict. Every failure mutates nothing. A bootstrap call that overlaps a running one is refused at once with the retryable saturated error (admission) and never queues.
  • Apply verifies authority after it holds the write permit and judges ownership, revision and expiry inside the SQLite transaction against a clock read after the database lock is held, so a request that waited for either lock cannot commit on a stale check or time. The receipt stores a hash of the whole apply request: a retry returns duplicate only when it matches exactly, and an altered name or preview ID fails with bootstrap_binding_mismatch. The mapping from the profile bearer to the created owner is Planned.

Session wait operation

Source candidate. session.wait is a read with no receipt. It takes sessionId, states (one to nine Session states), activities (one to four of stopped, waiting_approval, working, idle; at least one of the two is required, any other value fails validation) and waitMilliseconds (0 to 4000). Node rereads the Session every 200 ms and answers once the Session is in any of states or activities or the wait ends: { method, satisfied, session }, where satisfied: false carries the current Session. A missing Session fails resource_not_found. One call stays inside the 5-second request deadline; the caller repeats it to wait longer, as for turn.wait. Interrupting the call stops the rereads.

session.list, session.show and session.wait (which returns the same Session object as session.show) add two optional fields to each Session: activity (the Session's last announced activity, the value session.activity.changed carries) and latestTurnState (the state of the turn latestTurnId names, absent until a turn runs). No other operation returns them.

Source candidate (E13). The same two operations (and session.wait) add status and prompt. status is one of starting, working, compacting, rate_limited, idle, question, approval, error, done, ended and unknown: the value session.status.changed last announced. prompt is { approvalId, expiresAt, kind, options?, summary } of the approval the Session waits on, the text a person reads, bounded by the approval's own limits (8 KiB of text, at most 8 options). It appears only while status is question or approval, with a matching kind (question for the first, any other kind for the second), and only to a reader approval.show admits: a Session principal's session.list returns the status without the prompt. A session.list page names the prompt by approvalId, kind and expiresAt only, with no summary or options, because 100 Sessions of 8 KiB text pass the response limit; session.show and approval.show return the text. The field is status, not state, because a Session already has a lifecycle state (running, failed, stopped, ...); status is the derived view a person or bot acts on, and the lifecycle state stays the source of truth. Both are absent from every other operation's Session. A new field on a response changes the exported contract: a client and its Node upgrade together, because an older strict client rejects a Session that carries them.

Compatibility: the exported contract changes (session.wait also takes activities, and states becomes optional when activities is given), with no SDK source edit. A newer reader accepts an older Node's responses, because both fields are optional. An older strict SDK rejects a response once the Node populates the new fields, and an older Node rejects activities.

Turn queue operations

Source candidate (S26). turn.send keeps its operation ID and schema: it queues. Two operations join the session feature (schemas in packages/protocol/src/turn/contract.ts):

OperationRiskConfirmationReceiptSDK methodMCP tool
turn.queue.listreadnonenonelistTurnQueueturn_queue_list
turn.cancelreversiblenonerequiredcancelTurnturn_cancel
  • A Session holds at most MAX_SESSION_PENDING_TURNS (8) pending turns, the running one included. Queued turns dispatch in send order, each once; recovery and all seven handoffs complete within a 100 ms test-clock budget, and a Node restart keeps them queued.
  • turn.queue.list returns activeTurn (the dispatching or running turn) and queue in dispatch order, each entry with position, its own revision and an inputPreview of at most 160 UTF-8 bytes.
  • turn.cancel takes the queued turn's own revision, not the Session's, and records its own receipt. The cancelled turn reads failed with reason turn_rejected, the state an interrupt of a queued turn already produces.
  • Typed errors: turn_queue_full (429, not retryable) and turn_not_queued (409: the turn already left the queue). Both queue and mutate nothing.
  • Steer is Planned (S26b, D-144); turn.send has no mode.

Connection grant and pool operations

Source candidate. Six release-disabled operations (releaseQualified: false, CLI-only projection) in the connection feature (packages/protocol/src/connection), specified by D-147. A grant offers one Connection to one exact scope: an organizational Workspace (container_… today) or a Project. A pool names which granted Connections a scope's Sessions rotate over.

OperationRiskConfirmationReceiptSDK method
connection.grant.listreadnonenonelistConnectionGrants
connection.pool.listreadnonenonelistConnectionPools
connection.grant.set.previewreversiblenonenonepreviewConnectionGrant
connection.grant.set.confirmconsequentialtrusted-client-requiredrequiredconfirmConnectionGrant
connection.pool.set.previewreversiblenonenonepreviewConnectionPool
connection.pool.set.confirmconsequentialtrusted-client-requiredrequiredconfirmConnectionPool
  • A grant is active at an odd revision and revoked at an even one; a row is never deleted. A pool is off (no members), single (one member) or rotate (1–32 members); off keeps its row so a compare-and-set never meets a recreated pool.
  • The preview takes commandId, the exact scope and either granted or the pool's mode and members. It returns current (or none), the proposed state, expectedRevision, inputDigest, proof, issuedAt and expiresAt (30 seconds). The confirm repeats the input with the digest, proof and revision; its receipt names the confirm method and target type connection_grant or connection_pool.
  • A retried preview (same commandId, actor, operation and input) returns a fresh proof with the original issuedAt and expiresAt and retires the earlier proof. The same commandId on another actor, operation or input is storage_conflict.
  • Refused with storage_conflict and no change: a no-op request, a revoke while the account is a pool member of that scope, a pool member without an active grant to that scope, and a stale expectedRevision. An unknown scope or Connection is resource_not_found.
  • Admission: a Session binds a Connection only when an active grant offers it to the Session's Project or its Workspace; otherwise it fails closed with configuration_invalid. A Project's pool overrides its Workspace's, and no default crosses Workspaces. Rotation picks among the pool's members at Session admission only, by the usual account order (least usage, then free slots, then name) and each account's own cap. A revoke also stops the next launch, recovery and queued-turn dispatch on that account (queued turns fail turn_rejected). The grant check and the claim of a queued turn share one synchronous storage call, so a revoke never falls between them. A turn already claimed (dispatching) or running finishes: the validation after the claim is asynchronous, and a revoke that lands there does not stop its prompt. A launch past its check finishes too: launch preparation is asynchronous, so a revoke can land between the check and the spawn. maia session stop ends a live runtime. Maia never rewrites a harness credential file.

Scheduled Session operations

Source candidate (S30). Ten release-disabled operations (releaseQualified: false) in the routine feature (packages/protocol/src/routine); the Node store is migration 65. Decision: D-142.

OperationRiskConfirmationReceiptSDK methodExposure
routine.listreadnonenonelistRoutinesCLI, coordinator
routine.showreadnonenoneshowRoutineCLI, coordinator
routine.create.previewreversiblenonenonepreviewRoutineCreateCLI
routine.create.confirmconsequentialtrusted-client-requiredrequiredconfirmRoutineCreateCLI
routine.pausereversiblenonerequiredpauseRoutineCLI
routine.resumereversiblenonerequiredresumeRoutineCLI
routine.disable.previewreversiblenonenonepreviewRoutineDisableCLI
routine.disable.confirmconsequentialtrusted-client-requiredrequiredconfirmRoutineDisableCLI
routine.run.previewreversiblenonenonepreviewRoutineRunCLI
routine.run.confirmconsequentialtrusted-client-requiredrequiredconfirmRoutineRunCLI
  • A routine is routine_ plus 32 hex characters, derived from the actor and the create commandId, so a retried create names the same routine. States are active, paused and disabled; nextFireAt is present exactly while the routine is active. The template names a Project and a prompt, plus an executor or a connectionId, and a model only with a connectionId.
  • The preview returns routineId, up to 3 nextFires, inputDigest, a one-time proof and a 2-minute expiry. The digest binds the actor, command ID, routine ID, name, paused, schedule and template; the Node keeps only a hash of the proof and deletes it on confirmation. Create without a valid preview fails and mutates nothing.
  • Confirmation checks the proof, digest, actor and expiry, then inserts the routine at revision 1 in one transaction with its receipt (target type routine). The optional create input paused: true inserts it paused with no nextFireAt; a resume then schedules the first fire, so a routine never fires before someone releases it. pause, resume and disable.confirm compare-and-set expectedRevision and record a receipt at the new revision. resume sets nextFireAt to the first slot after the resume time, so it never replays a slot missed while the routine was paused.
  • disable is final, so it previews first. routine.disable.preview binds the actor, command ID, routine ID and expectedRevision, names the routine, and returns inputDigest, a one-time proof and a 2-minute expiry. Only routine.disable.confirm with that proof, digest, command ID and revision disables the routine; a replay with other values fails with storage_conflict. A preview of a routine that is disabled or at another revision fails the same way and issues no proof.
  • The closed error set of a refused create is routine_schedule_invalid, routine_interval_too_short (consecutive fires under 15 minutes apart), routine_limit_reached (20 active or paused routines in the Project) and configuration_invalid: a template its Connection can never serve, under the rule session.create applies (a gateway without executor, an executor other than a vendor account's, or a model on a vendor account). The preview and the confirmation both check it. A stale or expired preview and a stale expectedRevision fail with storage_conflict; an unknown routine, Project or Connection fails with resource_not_found.
  • Each due slot records one routine_fires row, the evidence of what happened: launching, then started with the Session and first turn, or failed with reason connection_disabled, held, launch_failed, outcome_unknown, routine_not_active, saturated or workspace_unavailable, or skipped with reason overlap. routine.show returns the newest 20, and each started fire carries the first turn's current state and, when that turn failed, failureReason and the runtime's failure (code and message) as turn.show carries them. A fire with a Session that failed before any turn carries that Session's failure as session.show carries it, without failureReason. The claim of a slot and the advance of nextFireAt commit in one transaction, so a slot fires at most once.
  • A full Node refuses session.start with the retryable saturated error, the code runtime admission uses. The fire stays launching as a pending admission and retries each tick until its deadline, 10 minutes after the claim on the Effect clock, then records reason saturated. A tick makes at most 32 launch attempts: fresh claims first, so a due routine starts within ceil(due / 32) ticks, and pending retries with the rest. After a crash, a launching fire resumes as started only from a turn.send receipt by its routine's scheduler actor on the Session the fire attached; any other fire records outcome_unknown. Each scheduler tick stops the live Session of a settled fire whose turns are all its own and finished, through session.stop at the revision the eligibility query read.
  • A manual run (FV8) fires an active routine once now. It starts a Session, so it previews first, as disable does: routine.run.preview binds the actor, command ID, routine ID and expectedRevision and returns the routine's name, inputDigest, a one-time proof and a 2-minute expiry. routine.run.confirm spends that proof in one transaction that requires the same revision, judges the run as the next cron fire would be judged, records a launching fire with triggerKind: "manual" and scheduledAt at the record time, and its receipt (target type routine_fire; the fire ID derives from the actor and commandId). The scheduler's next tick claims it and launches it through the cron path, so there is no second scheduler. The claim judges it again, as it judges a cron slot: a routine not active at the claim fails it with routine_not_active, a hold with held, a disabled Connection with connection_disabled. A due cron slot is judged first in that claim, so a slot that meets an unsettled manual fire is skipped with overlap, and a run never moves nextFireAt. Only cron fires keep one row per slot. A replay with the same commandId and proof returns the same fire; other values, or a stale, expired or spent proof or another revision, fail with storage_conflict. The preview and the confirmation refuse and record nothing with routine_busy (retryable) while the previous fire's first turn is unsettled, routine_not_active, held, connection_disabled or resource_not_found; a refused confirmation keeps its proof until it expires. An unclaimed manual fire survives a restart for the next tick to judge. Neither operation is a coordinator tool.
  • The scheduler acts as the Node's automation (an actor kind with no credential, one ID per purpose). Its receipts name actor_node-automation_routine-scheduler_<routineId>, never actor_local_user. It may workspace.list and session.create in its fire's Project, creating with only a Workspace, executor or connectionId, and model; then session.show, session.start, session.stop and turn.send on its fire's own Session. Anything else fails role_denied.

Webhook operations

Source candidate. Four release-disabled operations (releaseQualified: false) in the webhooks feature (packages/protocol/src/webhooks), E12:

OperationRiskConfirmationReceiptExposureSDK method
webhooks.listreadnonenoneCLIlistWebhooks
webhooks.deliveriesreadnonenoneCLIlistWebhookDeliveries
webhooks.change.previewreversiblenonenoneCLIpreviewWebhookChange
webhooks.change.confirmconsequentialtrusted-client-requiredrequiredCLIconfirmWebhookChange
  • All four are operator-only: an attached Session credential is refused.
  • The change is a union on kind: add { spec }, and remove and test { webhookId, expectedRevision }, and redeliver { deliveryId, expectedRevision }; the webhook is the one that owns the delivery. The preview binds the action, input digest, actor, hook revision, destination and expiry; confirmation compares the revision and commits in one transaction. The effect class of the confirm is external-uncertain.
  • Webhook IDs match hook_[a-f0-9]{32}. The receipt names method webhooks.change.confirm and target type webhook. A repeated commandId replays the stored result with delivery: "duplicate".
  • The record holds the secret's variable name, never its value.

Agent definition operations

Source candidate. Five release-disabled operations (releaseQualified: false) in the agents feature (packages/protocol/src/agents), specified by S49 and D-120:

OperationRiskConfirmationReceiptExposureSDK method
agents.detectreadnonenoneCLIdetectAgents
agents.listreadnonenoneCLI, coordinatorlistAgents
agents.showreadnonenoneCLI, coordinatorshowAgent
agents.change.previewreversiblenonenoneCLIpreviewAgentChange
agents.change.confirmconsequentialtrusted-client-requiredrequiredCLIconfirmAgentChange
  • agents.detect takes no parameters and returns harnesses (claude-code and codex-cli: found, version, folders of { scope, path }) and scopes.user (ok, or unavailable with a bounded reason, failed, timeout, output_too_large, invalid_output or unsupported). It is operator-only like connection.detect: an attached Session credential is refused before any process runs. The result holds no Skill name, binary path, account or raw error text. Project scope is deferred to AI1c.
  • The change is a union on kind: create { projectId, spec }, revise { agentId, expectedRevision, spec } and disable { agentId, expectedRevision }. The spec schema (agentSpecSchema) is the only definition of a valid Agent. A preview lives 30 seconds, binds inputDigest to the change, actor and commandId, and returns a proof the Node stores hashed. Confirmation checks the proof, then compares the expected revision and commits in one transaction.
  • An actor and commandId hold one open preview. Repeating the same change inside its 30 seconds returns the same reply (Agent ID, inputDigest, proof and expiry), so a lost or late reply never retires the proof another reply carries, and takes no extra preview capacity. The Node derives the proof from a key it keeps in memory; after a Node restart inside the window, a retry returns a proof of the new key and the earlier proof stops working. A different change under that commandId fails with storage_conflict and leaves the open preview as it was.
  • Agent IDs match agent_[a-f0-9]{32} and the Node picks them. The receipt names method agents.change.confirm, target type agent and the new revision; receipt.list filters on it. A repeated commandId replays the stored result with delivery: "duplicate", even after the proof expired. An unknown, reused or expired proof, a proof bound to another change, actor or commandId, and a revision that moved fail with storage_conflict and change nothing.
  • session.create accepts an agentId and pins the Agent ID and revision to the Session. The Node checks the executor for the revision it read; a revision made after that check fails with storage_conflict and creates no Session. readAgent and readAgentRevision in services/node/src/agents are the in-Node read API for a later Routine (S30, S50) to name an Agent without a store of its own.

Events, traces, and cursors

The shared event envelope, journal, cursors and federation are specified in Event core (Planned, D-188); the records below are the target they evolve.

events.read (source candidate, EV0c-1; packages/protocol/src/event) reads the journal by signed cursor, event ID or sequence. Its result is a page, a cursor_reset_required or an invalid_cursor, each with status.

Today the Node API streams observe.events over /api/v1/observations (the maia.node.observations.v1 WebSocket subprotocol) with snapshot, event, heartbeat, gap, reset, error and summary frames, an opaque signed cursor, at most 64 records per batch and 256 KiB per frame (packages/protocol/src/local-node). The same socket serves session.attach (source candidate, S12b; packages/protocol/src/session-stream): a subscribe names one Session and an optional { epoch, sequence } cursor, and the Node answers attached (replay or snapshot), records frames (at most 64 SessionStreamRecords), synchronized (the position and each open item's text so far), then live records and heartbeats, ending with a terminal resync or error. Records are turn.started, item.started, item.delta (live only), item.completed and turn.completed, each with the Session, turn, owner sequence and Node epoch; items are message, reasoning (visible summary only), tool_call, command, file_change, approval_request, error and usage, with an optional namespaced extension. The records below are the target.

Every durable transition emits an event in the owning aggregate's transaction. EventV1 below is superseded by maia.event.v2 (Event core, D-188), which has the same single discriminator and maps each field; it was never emitted:

interface EventV1<TType extends string = string, TPayload = unknown> {
  readonly schemaVersion: "maia.event.v1";
  readonly eventId: string;
  readonly eventType: TType;
  readonly ownerType: string;
  readonly ownerId: string;
  readonly ownerSequence: number;
  readonly aggregateRevision: number;
  readonly projectId?: string;
  readonly sessionId?: string;
  readonly activationId?: string;
  readonly attemptId?: string;
  readonly actorId?: string;
  readonly causationId: string;
  readonly correlationId: string;
  readonly classification: "public" | "operator" | "project" | "sensitive";
  readonly occurredAt: string;
  readonly payload: TPayload;
}
  • Owner sequence is gap-free. Cross-owner order is never inferred from clocks; correlation and causation IDs express relationships.
  • A stream cursor is an opaque signed encoding of owner, last sequence, filter digest and expiry. An expired or compacted cursor returns cursor_reset_required with a fresh bounded snapshot descriptor; clients never guess a sequence.
  • TraceEventV1 is a redacted evidence projection of an event, effect, model stream item, tool call, provider attempt or diagnostic. It always carries correlation IDs and the source event, effect or receipt ID when one exists, and never credentials, authorization headers, hidden reasoning, raw environment dumps, raw audio or unbounded payloads.

All application-facing live streams use the observation contract:

type ObservationDomainV1 =
  | "agent"
  | "swarm"
  | "node"
  | "provider"
  | "voice"
  | "a2a"
  | "security"
  | "storage"
  | "scheduler";

type JsonValueV1 =
  | null
  | boolean
  | number
  | string
  | ReadonlyArray<JsonValueV1>
  | { readonly [key: string]: JsonValueV1 };

interface ObservationRecordV1 {
  readonly schemaVersion: "maia.observation.v1";
  readonly observationId: string;
  readonly domain: ObservationDomainV1;
  readonly importance: "authoritative" | "diagnostic";
  readonly eventType: string;
  readonly nodeId: string;
  readonly nodeEpoch: number;
  readonly partitionId: number;
  readonly partitionSequence: number;
  readonly ownerType: string;
  readonly ownerId: string;
  readonly ownerSequence: number | null;
  readonly projectId: string | null;
  readonly sessionId: string | null;
  readonly activationId: string | null;
  readonly attemptId: string | null;
  readonly effectId: string | null;
  readonly commandId: string | null;
  readonly receiptId: string | null;
  readonly connectionId: string | null;
  readonly causationId: string;
  readonly correlationId: string;
  readonly classification: "public" | "operator" | "project" | "sensitive";
  readonly occurredAt: string;
  readonly observedAt: string;
  readonly schemaRef: string;
  readonly payloadDisposition: "inline" | "artifact" | "redacted";
  readonly payload: JsonValueV1;
  readonly artifactRef: string | null;
  readonly coalescedRecordCount: number;
  readonly droppedRecordCountBefore: number;
}

interface ObservationQueryV1 {
  readonly domains: ReadonlyArray<ObservationDomainV1>;
  readonly importance: ReadonlyArray<"authoritative" | "diagnostic">;
  readonly projectIds: ReadonlyArray<string>;
  readonly sessionIds: ReadonlyArray<string>;
  readonly eventTypes: ReadonlyArray<string>;
  readonly classifications: ReadonlyArray<
    "public" | "operator" | "project" | "sensitive"
  >;
  readonly includeContent: boolean;
  readonly limit: number;
}

interface ObservationWatermarkV1 {
  readonly topologyRevision: number;
  readonly partitionSequences: ReadonlyArray<{
    readonly partitionId: number;
    readonly sequence: number;
  }>;
  readonly evidenceProjectedThrough: ReadonlyArray<{
    readonly partitionId: number;
    readonly sequence: number;
  }>;
  readonly coverage: "complete" | "degraded" | "reset-required";
}

interface ObservationBatchV1 {
  readonly records: ReadonlyArray<ObservationRecordV1>;
  readonly resolvedFilterDigest: string;
  readonly watermark: ObservationWatermarkV1;
  readonly coalescedRecordCount: number;
  readonly droppedRecordCount: number;
  readonly heartbeat: boolean;
}

interface ObservationSnapshotV1 {
  readonly records: ReadonlyArray<ObservationRecordV1>;
  readonly resolvedFilterDigest: string;
  readonly watermark: ObservationWatermarkV1;
  readonly generatedAt: string;
  readonly truncated: boolean;
}

type ObservationCursorV1 = string & {
  readonly ObservationCursorV1: unique symbol;
};
  • Empty filter arrays mean "all authorized values" only for reads and never grant authority. Node intersects the query with the actor's grants and returns the resolved filter digest. includeContent is separately authorized and defaults to false.
  • ObservationCursorV1 signs Node and epoch, topology revision, resolved filter digest, authorization-policy revision, watermark vector, issue and expiry time, and actor and device audience. It is invalid for another filter, actor, device or policy revision.
  • ObservationBatchV1 holds 1–128 ordered records from one or more partitions, or zero only when heartbeat: true, plus the post-batch watermark and coalescing and gap counters. Order exists only within a partition or by owner causation; array position never implies cross-partition causality.

Internal bulk consumers persist:

interface ObservationProcessorCheckpointV1 {
  readonly processorId: string;
  readonly processorVersion: string;
  readonly partitionId: number;
  readonly acknowledgedSequence: number;
  readonly inputSchemaDigest: string;
  readonly projectionRevision: number;
  readonly state: "running" | "lagging" | "blocked" | "rebuilding";
  readonly updatedAt: string;
}

interface ObservationProcessorDescriptorV1 {
  readonly processorId: string;
  readonly processorVersion: string;
  readonly criticality: "required" | "optional";
  readonly domains: ReadonlyArray<ObservationDomainV1>;
  readonly importance: ReadonlyArray<"authoritative" | "diagnostic">;
  readonly eventTypes: ReadonlyArray<string>;
  readonly inputSchemaDigest: string;
  readonly outputSchemaRefs: ReadonlyArray<string>;
  readonly checkpointVersion: number;
  readonly maxBatchRecords: number;
  readonly maxBatchBytes: number;
  readonly maxAttempts: number;
  readonly retryBackoffMs: number;
  readonly deadLetter: "block" | "quarantine-and-continue";
  readonly allowedOperationIds: ReadonlyArray<string>;
}
  • Delivery is at least once. Projection writes and checkpoint advance commit atomically; processors are idempotent by processor, partition and sequence.
  • A required processor uses deadLetter: "block"; an optional one may quarantine a bounded batch and emit a diagnostic. Output is a projection or one of its allowed normal operation requests, never an unreceipted mutation.
  • A descriptor has 1–16 domains (empty eventTypes means every authorized type in them), batches of at most 128 records or 512 KiB, maxAttempts 1–10, backoff 100–60000 ms and at most 32 operation IDs. Admission pins its digest, implementation digest, owner, capability evidence and revision outside Project configuration.
  • Required evidence is never sampled; sampled debug logs publish dropped-record counters. A consequential transition whose required event, receipt or outbox row cannot commit fails. JSONL projection lag degrades health but does not invalidate a committed receipt; the outbox must catch up before retention or backup advances its watermark.

Lifecycle baseline

Every lifecycle uses a registry-owned transition table with caller, preconditions, expected revisions, event, receipt, timeout and recovery path. Today the Session schema uses idle, starting, running, waiting-approval, interrupted, recovering, stopping, stopped and failed (packages/protocol/src/session/effect-schema.ts). The closed top-level target states are:

AggregateStates
Projectcandidate, admitted, active, held, draining, archived
Sessioncreated, queued, starting, running, idle, waiting, blocked, stopping, outcome-unknown, reconciling, completed, failed, cancelled, archived
Activationadmitted, queued, assigned, starting, running, waiting, interrupting, outcome-unknown, reconciling, succeeded, failed, cancelled, blocked
Effectplanned, started, succeeded, failed, outcome-unknown, reconciled
Messageaccepted, available, claimed, processed, dead-letter, expired
RoutineTickscheduled, admitted, dispatching, session-linked, completed, skipped, coalesced, blocked, failed, cancelled, outcome-unknown, reconciling
Attachmentopen, closing, closed, expired, revoked
Connectionpending, active, degraded, revoked, logged-out

Terminal or archived parents cannot create children. waiting means the activation yielded for a registered interaction, message or timer. blocked means policy or evidence prevents progress and needs a recorded unblocker. idle means no activation holds the Session lease.

Worker protocol

Today the managed Pi worker speaks protocol version 1 as line-delimited JSON on stdin and stdout (packages/agent/src/managed-worker/protocol.ts): commands initialize, prompt, interrupt and shutdown; events initialized, turn.started, turn.completed, turn.interrupted, shutdown.complete and error; 64 KiB per command and 1 MiB per output. The target below extends it.

Target: length-bounded NDJSON over inherited pipes. Every frame has protocolVersion, workerVersion, requestId, a monotonic sequence and one discriminated type:

hello | initialize | initialized | prompt | event | tool-call | tool-result
observation-batch | interrupt | interrupted | heartbeat | shutdown | terminal
error | ack
  • Limits: 1 MiB per frame, 16 KiB instruction or message content, 64 KiB inline tool result, 256 unacknowledged authoritative event frames, and one diagnostic queue of at most 2,048 records or 8 MiB. observation-batch carries at most 128 diagnostic records or 512 KiB and is sent without waiting for a client. Larger values become artifact references.
  • Timing: initialization within 15 seconds; heartbeats every 5 seconds, and three misses fence the worker; graceful interrupt gets 10 seconds, then process-group terminate 5 seconds before kill. A restart always creates a new attempt and never reuses an assignment epoch.
  • initialize carries one immutable AgentActivationV1, admitted history, the resolved tool manifest, a provider credential handle with expiry and the execution profile; never a Node database handle or ambient Pi state.
  • Pi SDK messages, streams, usage, tool calls, interruption, compaction and provider errors are normalized before leaving the adapter. Authoritative event frames reference their operation or effect and replay by request and sequence. Diagnostics coalesce into observation-batch; saturation is reported in the next batch and Node emits an authoritative gap.
  • Stdout is protocol-only. Stderr is a bounded crash and bootstrap fallback, not the live feed.

Machine principal, lease and evidence (MN0)

Status: Source candidate schemas in packages/protocol/src/machine-principal/ and packages/protocol/src/machine-lease/; every schema rejects unknown fields. No operation, transport or Node handler uses them yet: MN1 (enrollment) and MN2 (placement) add those. They keep what a remote Execution Node may be told and may report apart from how that is transported.

Bounds

A bound is sourced from the named document or new policy: a value MN0 chooses and MN1/MN2 may change only with a decision and a matching fixture.

BoundValueBasis
Heartbeat interval5 sSourced: swarm heartbeats, operations
Heartbeat misses that stop placement4Sourced: same
Pairing challenge; rotation start5 min; 50% of TTLSourced: operations; MN1 owns them
Lease grant TTL (minimum; maximum)27 s; 30 sNew policy; the minimum is the least that keeps the credited window below
Clock error budget2 sNew policy; covers monotonic rate error and timer granularity, never wall-clock offset
Maximum request round trip5 sNew policy (one heartbeat interval)
Bounded stop after the local deadline10 sNew policy (the graceful-interrupt window of the worker protocol)
Spool records; bytes; oldest unacked10,000; 64 MiB; 24 hNew policy
Grants per principal; capabilities each16; 8New policy; the largest context.show projection stays under its 16 KiB cap
Capability code; identifier length32; 122 charactersNew policy; 122 keeps actor_<id> within the 128-character identifier limit

The credited window minimum TTL - round trip - clock error budget (27 - 5 - 2 = 20 s) equals heartbeat interval x misses (20 s): every accepted grant leaves the Node a local lease of at least the span four missed heartbeats cover. The schema refuses a grant outside the minimum and maximum TTL, and a test pins the relation. Lease timestamps carry at most millisecond precision, the precision the TTL arithmetic compares. There is no reassignment bound: no constant lets a replacement launch.

Assignment identity

MachineAssignment names the exact Control Node and registration incarnation, the Project, the Execution Environment (environmentId, which holds the existing workspace_* identity until the naming migration), environmentRevision and environmentDigest. The tuple (controlNodeId, environmentId, environmentRevision) names one immutable definition: a change makes a new revision, and a revision's digest never changes. environmentDigest is the sha256: digest of that revision's canonical definition, the provenance a Node checks. The Node starts the assignment only when the environment it prepared, or received through authorized, digest-checked artifact transfer, has exactly that digest; otherwise it refuses and reports. The definition format and its canonical encoding belong to MN2's preparation operation. MN0 fixes only that the digest names an immutable revision and is compared by equality.

Lease semantics

  1. A grant's duration is expiresAt - issuedAt, with minimum TTL <= duration <= maximum TTL. A grant (MachineLeaseGrant) also carries an increasing renewalSequence, the revocationRevision, a cumulative spool acknowledgedThrough and the requestId of the Node request it answers. The timestamps represent the duration; no party compares them with a wall clock to decide expiry.
  2. The deadline is anchored to the request. A one-way timestamp cannot separate clock offset from delay: a Node 10 s behind, hearing a grant 12 s late, sees it as 2 s old and would end its lease 8 s after Control's. So the Node asks for every grant, acquire and renew alike, with a requestId it has never used before and never reuses. It takes a monotonic reading when it first sends the request and another when the grant arrives. It refuses a grant that answers no outstanding request, or took longer than the maximum round trip; a refusal leaves the earlier deadline as it was. Control issues the grant after the Node sent the request, so send + duration never falls after Control's expiry, given item 4.
  3. The local deadline is first-send reading + duration - clock error budget. A retry of an unanswered request keeps its requestId and its original first-send reading, so a later retry never moves the anchor later: Control may answer the retry with the grant it issued to the first send, and a deadline anchored to the retry would outlive that grant. The Node refuses a grant whose remaining lifetime (deadline - receive reading) is not positive; within the round-trip bound the minimum TTL leaves at least 20 s.
  4. Control enforces durations with its own suspend-aware monotonic clock, never by comparing a timestamp with its wall clock. For each assignment it keeps one horizon: the latest issue reading + duration over the last grant a running report confirmed (item 9) and every grant issued since, because the Node may hold any of them. A later, shorter grant never lowers the horizon: a 30 s grant issued at t=0 and a 27 s renewal issued at t=0.5 whose response is lost leave the horizon at t=30, not t=27.5, while the Node may still run to t=28. A confirming report drops the grants it replaced. If Control has no monotonic source that counts across suspend, it issues no grant. A discontinuity is a Control restart, a resume, or a wall and monotonic disagreement beyond the clock error budget. At one, Control sets every horizon to now + maximum TTL, which is late for any grant it issued before, whatever its duration, and raises a placement barrier on every assignment. A barrier clears only when an authenticated running report confirms a grant issued after the discontinuity (item 9). A step in Control's wall clock therefore never makes expiry earlier: a wall clock stepped 10 s forward would expire the 30 s lease issued at t=1 at t=21 while the Node runs to t=28, and the monotonic horizon stays at t=31 until the disagreement is detected, when it moves later and blocks placement.
  5. The Node fails closed where it cannot establish those bounds. Its monotonic source must keep counting while the host is suspended; with no such source, or after a restart between send and receive, it refuses the grant and admits no work.
  6. Suspend and resume. Wall-clock time serves the Node only as a tripwire: when the wall clock and the monotonic clock disagree by more than the clock error budget since the last accepted grant, or the host reports a resume, the Node treats the lease as expired at once. It admits no new work and starts the bounded stop of item 8. A later grant makes a lease valid again but never cancels a stop already begun or revives a stopped worker.
  7. It applies a grant only when (leaseEpoch, renewalSequence) is greater than the last applied pair for that assignment, and revocationRevision is not below its own high-water mark. A replay, the same requestId and the same pair delivered again, is acknowledged and changes nothing: it never recomputes or extends a deadline. An equal pair under a different requestId, or a lower pair, is refused. The report names the grant it applied by appliedRequestId and appliedRenewalSequence. The schema parses each grant alone, so this ordering is the Node's and Control's state to enforce.
  8. At the local deadline the supervisor admits no new work and starts a bounded stop: phase: "stopping", then "stopped" once process-tree cleanup is confirmed, within the stop bound. Failing that it keeps reporting stopping, and the assignment stays uncertain.
  9. Placement depends on lease validity as well as on heartbeats. The Control Node places new work on a Node only while fewer than four consecutive heartbeats are missed, Control's horizon (item 4) has not been reached, no placement barrier stands, no revocation or fence applies, and the Node's last authenticated report is running and confirms the newest grant issued for that assignment. A report confirms a grant when its appliedRequestId, appliedRenewalSequence, assignment and lease epoch equal that grant's. Issuing a grant proves nothing about the Node: a lost response, or a report that names an older request or sequence, leaves placement blocked until a later report confirms the newest grant. A heartbeat shows the Node is reachable, not that its lease holds. Neither missed heartbeats nor a Control-side fence prove the process stopped.
  10. A replacement launches only after cleanup reconciliation (the Node reports stopped for that assignment and epoch) or an explicit expiry reconciliation that MN2 defines with an overlapping-execution test. Expiry alone never authorizes a launch, because execution can continue after Control's expiry: with the request sent at t=0, the grant issued at t=1 and a 30 s duration, the Node's deadline is t=28 (2 s budget), Control's expiry is t=31, and the bounded stop may run to t=38. A local checkpoint never renews a lease.

Fixtures, all pending MN2 because they need a stateful Node and Control (manifest): lease-delayed-delivery (the 10 s behind, 12 s late example), lease-unmatched-grant, lease-retry-anchor (a retry keeps the first-send reading), lease-replay (no deadline change), lease-renewal-order, lease-control-clock-step (a 10 s step in Control's wall clock never makes expiry earlier: expiry defers and placement is blocked), lease-shorter-renewal-lost (a lost 27 s renewal after a 30 s grant leaves the horizon at the 30 s grant's; a discontinuity waits the full maximum TTL), lease-grant-response-lost (a fresh grant after a discontinuity whose response is lost, beside an old running report, keeps placement blocked), lease-stale-report (a running report naming an older request or sequence does not confirm the newest grant), lease-suspend-resume and lease-placement-validity.

Evidence and spool semantics

  • Each record has a per-source sequence, contiguous from 1, so a cumulative acknowledgement (acknowledgedThrough) is unambiguous. It and the Node's truncatedThrough obey truncated <= acknowledged <= last. A Node removes only acknowledged records, and no age rule removes an unacknowledged one.
  • The Node holds exactly the records after truncatedThrough, so a status reports retainedRecords = lastSequence - truncatedThrough. Records and bytes are both zero or both positive, and oldestUnacknowledgedAgeMs is 0 once acknowledgedThrough = lastSequence. A status that says otherwise reports unacknowledged writes with no retained evidence and is invalid.
  • Reaching the record, byte or oldest-unacknowledged-age bound sets admission: "stopped": the supervisor admits no new work and either interrupts safely within a bound or reports explicit uncertainty. A status that reaches a bound while open, or exceeds the record or byte quota, is invalid. No value represents a silently dropped record.

Identity generations

GenerationMinted by, whenChanges onDetects
Durable Control identity (controlNodeId)First-owner bootstrapNever; a restore keeps it. The stored node_epoch is the bootstrap process's, not the current oneA grant or certificate for another Control Node
Process epoch (instanceId; Node's processEpoch)Each process startEvery restartStale cursors, bearer and in-flight deliveries
Registration incarnationControl Node at enrollmentRe-enrollment, never rotationA wiped Node re-enrolled under the same Node ID
Credential/revocation generation (revocationRevision)Control Node, per registrationRotation, grant change, revocationA stale certificate or grant
Worker incarnationExecution Node, per startEvery worker startLate events from a previous worker
Lease epoch and renewal sequenceControl NodeAcquire, handoff, renewalA stale writer

Restore and rollback

From one side, ordinary lag and a rollback look alike, and a restore of both sides looks like neither. The rule therefore never depends on detecting a restore.

  • Stale credential (ordinary lag). After a rotation, grant change or revocation, a Node that was offline holds a lower revocationRevision than the Control Node. That proves nothing about a restore. The Control Node compares every presented certificate and grant with its current record and refuses a stale one; the Node recovers by rotation or renewal, or by a fresh enrollment after a revocation.
  • Proven rollback. Only a contradiction proves one: a Node reporting an observedRevocationRevision above the Control Node's record (the Control Node was restored), or a lastSequence below an acknowledgement the Control Node already recorded (the Node lost evidence). Either marks the registration recovery-required: no grant, renewal or machine credential is admitted for it until administrator recovery ends in a fresh enrollment, with a new registration incarnation and certificate.
  • Restore is administrator recovery. Restoring a Control Node or an Execution Node from a backup is never transparent. The restore procedure (operations), not detection, marks every registration of a restored Control Node recovery-required and makes a restored Execution Node discard its enrollment, spool and high-water store; each ends in a fresh enrollment. That holds when no contradiction exists: with both sides restored to the same point, a certificate revoked after the backup would otherwise be valid again. Copying state back outside that procedure is not detected and is outside the guarantee.
  • No transparent recovery. It would need a trusted recovery generation held outside the restored state. None exists, so none is offered; MN1 may add one only with E2a's secret store and a decision.

Fixtures, all pending MN1 because they need enrollment and a restore procedure: restore-ordinary-lag, restore-unilateral (Control alone, and Node alone), restore-matching and restore-revoked-credential (manifest).

MN1 gap inventory

What MN1 (enroll, inspect, rotate and revoke an exact Node) still needs. Nothing below is built, and no operation ID is reserved for it.

NeedExists todayMissing before MN1 can ship
Operationscontext.show (client, session and machine projection); team-workspace.bootstrap.preview and .apply (human-only, release-disabled); node.remote.* (SSH install or registration of a remote Node, D-080, D-176, not enrollment)Challenge, enroll, inspect, rotate and revoke for one exact Node, plus the machine reads, lease renewal, report and evidence operations. Each is incomplete until its protocol entry, SDK method, Node handler, receipt policy, contract tests and CLI projection agree.
Managed administratorA first owner, a User, Device and client row, and a bearer that resolves to none of them (specification)A verified administrator credential on a running Node. The bearer is transport authentication, and the inherited descriptor of D-136 exists only while the CLI starts the Node, so neither can confirm an enrollment (first version: the D-150 ceremony). E2a's managed secret store comes first (ROADMAP), then the managed client credential and the bearer-to-owner mapping.
Pairing and key bindingThe documented target: a one-use, five-minute challenge and a Node-generated key pair (operations)The challenge format and storage (digest only), proof of possession of the Node's key, the certificate authority and its key custody (E2a), and how tailnet identity evidence is checked and recorded, as evidence and never as authentication.
Pre-credential admissionA loopback listener that authenticates a bearer before it reads a bodyA tailnet listener that admits a joining Node with no credential yet: one route, bounded body and attempt counts, replay and expiry denial, no Funnel, and a decision on mTLS termination.
Certificate lifecycleTargets only: rotation from 50% of the TTL, immediate revocation that fences assignments and streamsThe certificate TTL, the per-request revocation check against revocationRevision, and the order in which a revocation fences leases, streams and the spool.
RestoreThe fail-closed restore rule aboveThe recovery-required registration state and the administrator recovery that ends in a fresh enrollment, the restore procedure that marks every registration, and the durable high-water store on the Execution Node that yields the contradiction signals.
Machine transport admissionA machine projection that no transport mintsThe machine operation set and its allow-list, so allowedActions stops being empty, and the transport that resolves a certificate to a principal.

Owner decisions still open: the operation grammar (execution-node.* with preview, confirmation and receipt, or a CLI-drafted plan); how the administrator credential reaches a running Node (a maintenance-window enrollment that restarts the Node with a fresh inherited descriptor is accepted meanwhile for the first version, with revoke and narrow on the local bearer until the managed administrator credential ends that exception, D-150); the certificate TTL; and the new-policy values in the table above.

Artifact and data lifecycle

Artifacts are immutable, content-addressed encrypted blobs with a metadata row:

interface ArtifactV1 {
  readonly artifactId: string;
  readonly digest: `sha256:${string}`;
  readonly mediaType: string;
  readonly sizeBytes: number;
  readonly classification: "operator" | "project" | "sensitive";
  readonly projectId: string;
  readonly ownerType: string;
  readonly ownerId: string;
  readonly encryptionKeyId: string;
  readonly provenance: ReadonlyArray<string>;
  readonly createdAt: string;
  readonly expiresAt?: string;
}
  • Imports reject private-network URLs, redirects across policy boundaries, unsupported media types, digest mismatch, archive traversal, malware, detected credentials and policy-exceeding size.
  • Download authorization is checked at request time. Garbage collection needs a preview, reference scan, tombstone and receipt.
  • RetentionPolicyV1 sets a TTL per event, trace, artifact, transcript, evaluation, export and receipt class, plus legal hold, encryption-erasure behavior and whether a digest or tombstone may remain. Raw audio retention is always false in v1. Redaction happens before persistence or external transmission; a failed required redaction fails closed. No class keeps Session conversation content after transit (D-168).

Schema-only inspection

Approved target for R1 (PF-26/DF-26); the CLI has no --schema option today. Schema inspection is an option on each registered resource command, not a separate contract command family (decision).

  • Syntax. --schema means --schema=output; the only other value is --schema=input (no spaced value). --json is accepted but redundant. Schema mode needs only a known registered command path, not target IDs, revisions, command IDs, confirmation material or required input.
  • Offline. Resolve schema mode after bounded command-path parsing and before execution arguments, stdin, configuration, profile or credential discovery, SDK transport creation or Node startup. Emit the installed CLI's bundled public schema. Never execute the command, start or contact a Node, contact a provider or network, load runtime plugins, query live permissions, reserve IDs, issue confirmations or receipts, write files or start workers.
  • Rejections. Reject runtime operands and options (including --profile, --node, stdin and confirmation flags and output paths), streaming or watch flags, duplicate selectors, unsupported values and unknown commands before any effect; never fall through to execution when a schema is missing. --schema-digest is not --schema. Recognize the option only at registered resource-command boundaries; tokens after -- and the maia agent passthrough grammar are not intercepted.
  • Output. On success stdout is exactly one complete JSON Schema document and a newline, exit 0, with a $schema dialect, a versioned $id and bundled references. On failure stdout is empty, exit is nonzero and stderr has a bounded error that never echoes secrets or full inputs. Never a partial schema or fetched references. Freeze output bounds in fixtures first.
  • Meaning. input describes the normalized public operation parameters, not CLI flags (that is --help). output describes the command's full ordinary non-streaming JSON success and failure responses, including the CLI envelope. Existing output formats do not change for this feature.
  • Generation. Generate or mechanically check the documents against the owning Effect schemas and exact descriptor versions; keep representable constraints, describe runtime-only refinements, and fail explicitly rather than widening a schema. Identical artifacts give identical bytes: no request IDs, time, user paths, credentials or live examples.
  • Reuse. SDK schema access and eligible model and voice tool descriptions reuse the same generated definitions, with distinct surface envelopes. No shelling out to the CLI, hand-written validators or universal model dispatcher. A static schema grants neither tool exposure nor invocation.
  • No live claims. Inspection makes no claim about another Node's version, capabilities, grants or release status. A release-disabled command may expose its schema; an unregistered one fails. Example plural commands do not become aliases.

Examples once R1 lands: maia session show --schema (no Session ID needed) and maia session create --schema=input. Commands such as maia workers list --schema follow only when their own slices exist.

Generation and conformance

The registry generates or mechanically checks:

ArtifactSource
Effect request/result/error typesoperation descriptors and referenced schemas
CLI grammar metadataoperation descriptors plus CLI presentation file
maia help --jsonregistry plus implementation capability manifest
SDK methodsoperation descriptors
Per-command --schema documents and SDK/tool schema projectionsowning Effect schemas, exact operation descriptors and surface envelope metadata
model action schemaseligible descriptors plus Agent policy
voice action schemaseligible descriptors plus Voice policy
event and receipt cataloguesdescriptor event/receipt metadata
OpenAPIdescriptors explicitly exposed over HTTP

Today scripts/quality/registry-parity.mjs (run by pnpm test:tooling) mechanically checks registry, SDK, Node handler and CLI help parity. The maintainer commands maia contract generate, maia contract check --json and maia contract fixtures verify are targets (CLI); until they exist, handwritten projections cannot be labelled generated.

  • Each operation fixture covers valid input and output, every typed error, preview and confirmation when applicable, emitted events, the receipt, a redaction scan, idempotent replay, stale-revision rejection and cross-version decode. Every vertical slice changes registry, service, SDK/CLI, docs, fixtures and tests together.
  • Observation fixtures also cover snapshot-then-events, aggregate filters, 32-partition watermark vectors, owner causality, duplicate batches, checkpoint atomicity, restart replay, diagnostic coalescing and gaps, slow-consumer disconnect, cursor expiry and reset, policy-revision changes, content-grant denial, Node epoch change, loopback WebSocket and remote WSS and schema equivalence.

Imported from Maia OS docs at 3dc3e07