Skip to main content

WebSocket commands

NodalMerge is WebSocket-first. This page documents the current runtime command contract from server routing and shared envelope structs. Use this with:
  • protocol/synchronization for lifecycle flow
  • protocol/blob-flow for transfer behavior

Endpoint and handshake

  • Route: GET /ws/:room_id (WebSocket upgrade)
  • First client frame must be a valid hello payload
  • If first payload is invalid/non-hello, server sends error (msg: "expected hello") and closes

hello (client -> server)

  • Required:
    • type
  • Common fields:
    • pubkey, known, subscribe, caps
    • peer_id — logical peer identity, stable across reconnects. Falls back to pubkey if omitted.
    • peer_type — free-form peer classification (e.g. "ui", "ephemeral-agent", "persistent-agent"). Falls back to "ui" if omitted.
  • Behavior:
    • If room is locked, token validation happens during hello handling
    • Capabilities are negotiated server-side from caps
    • peer_id/peer_type are echoed in peer-joined/peer-left broadcasts to other room members, and let the server target pack broadcasts at a specific logical peer across reconnects rather than a single socket session
    • On join, the server sends a full-state pack catch-up (an exported snapshot of the room’s node history) immediately after welcome, distinct from the diff-based catch-up described in protocol/synchronization

welcome (server -> client)

Envelope fields (from shared WelcomeEnvelope):
  • Required:
    • type: "welcome"
    • root
    • frontier
    • missing
    • caps
    • server_pubkey
    • peers
  • Optional:
    • mst_root
Server may immediately send a catch-up pack after welcome when needed.

Coverage status (current)

Currently routed client command types:
  • Core sync: subscribe, pack, request, mst-request, mst-done
  • Blob: blob-upload, blob-request, request-upload, blob-uploaded
  • Presence/relay: presence, webrtc-offer, webrtc-answer, webrtc-ice
  • Room admin: set-room-key, set-policy, server-info, start-tick, stop-tick, compact-room
  • Archive: archive.describe, archive.validate, archive.import, archive.export
  • Query/read-model: query.register, projection.build, projection.read, projection.invalidate, projection.list, replay.read-range
  • Topology: topology.create-child, topology.describe-lineage, topology.list-children, topology.propose-promotion, topology.validate-promotion, topology.apply-promotion
Graph introspection commands (GetFrontier, GetCausalParents, GetCanonicalResolution, ComputeSyncDiff, InspectPack, PromoteCheckpointToGraph) are available as HostCommand variants in nodalmerge-host-core for direct embedding use cases. See Graph introspection (host-core direct) below.

Core sync command schemas

subscribe

Request:
  • Required:
    • type
  • Optional:
    • patterns (defaults effectively to full scope when absent/invalid shape)
  • Success emits:
    • subscribe-ack

pack

Request:
  • Required:
    • type
    • nodes (base64-encoded postcard node pack)
  • Success emits:
    • pack relay to peers with fields from PeerPackRelayEnvelope (type, from, nodes, root)
  • Reject/error emits:
    • error with msg = "invalid pack: bad base64"
    • error with msg = "invalid pack: bad postcard"
    • close 4008 on peer rate-limit abuse

pack-ack (server -> client)

Acknowledges a submitted pack, reporting how many of its nodes were accepted.
  • accepted_count < incoming_count or rejected_count > 0 indicates a partial or full rejection — SDK clients should treat this as a signal to revert local optimistic state for the unaccepted nodes and resend.
  • The JavaScript SDK’s sync.push() tracks pending node IDs internally and reacts to this message automatically; see sdk/javascript for client-side handling.

request

Request:
  • Required:
    • type
  • Optional:
    • known (defaults to empty set behavior when omitted) Response (ServerPackReplyEnvelope):

mst-request

Request:
  • Required:
    • type
  • Optional:
    • paths Response:

mst-done

Request:
  • Required:
    • type
  • Optional:
    • missing_ids Response (when IDs present):
  • pack (type, from: "server", nodes, root)

Blob commands

blob-upload

Request:
  • Required:
    • type
    • blobs[] with hash and data_b64
  • Success emits:
    • blob-available broadcast with hashes

blob-request

Request:
  • Required:
    • type
    • hashes[]
  • Success emits one or both:
    • blob-redirect with redirects[] (hash, url, expires_at_unix)
    • blob-pack with blobs[] (hash, data) and requested[]

request-upload

Request:
  • Required:
    • type
    • hash
    • size
  • Optional:
    • content_type
  • Success emits:
    • upload-granted (hash, url, expires_at_unix)
    • or upload-denied (hash, reason)
  • Reject/error emits:
    • error for unsupported direct blob I/O negotiation or invalid hash

blob-uploaded

Request:
  • Required:
    • type
    • hash
  • Success emits:
    • blob-available broadcast
  • Reject emits:
    • upload-rejected (hash, reason) when verify fails

Presence and relay

presence

Request:
  • Required:
    • type
    • data
  • Success emits:
    • presence relay with fields from PresenceEnvelope (type, from, data)
  • Behavior:
    • Presence is ephemeral and not persisted in room DAG

WebRTC relay messages

Client commands:
  • webrtc-offer
  • webrtc-answer
  • webrtc-ice
Server behavior:
  • Normalizes relay payload
  • Adds from
  • Broadcasts to room; receiver selection is client-side via to
Example request:

Control-plane capability gates

Denied control-plane commands return:
Capability mapping:
  • set-policy -> policy.admin
  • set-room-key -> room.admin
  • start-tick, stop-tick -> tick.admin
  • archive.describe -> archive.read
  • archive.validate, archive.import, archive.export -> archive.admin
  • query.register, projection.build, projection.invalidate -> query.admin
  • projection.read, projection.list, replay.read-range -> query.read
  • topology.* -> topology.admin

Room, policy, and tick admin commands

set-room-key

Locks an open room with an Ed25519 verifying key. Client request:
  • Required:
    • type
    • pubkey
  • Optional:
    • none Success response:
Rejection paths:
  • error.msg = "reject.control_plane_forbidden: command=set-room-key requires=room.admin"
  • error with set-room-key-specific reason message for invalid key or already-locked room

set-policy

Installs room write policy. Client request:
  • Required:
    • type
    • default (allow or deny)
    • rules[] with path_glob
  • Optional:
    • can_write entries per rule Success response:
Rejection paths:
  • error.msg = "reject.control_plane_forbidden: command=set-policy requires=policy.admin"
  • payload-parse errors returned as error

server-info

Returns server identity info used for policy wiring. Client request:
  • Required:
    • type
  • Optional:
    • none Success envelope:

start-tick

Starts authoritative tick loop for room. Client request:
  • Required:
    • type
  • Optional:
    • interval_ms (defaults to server default)
    • intent_prefix (defaults to server default) Success response:
  • tick-started (new loop started)
  • tick-already-running (idempotent no-op)

stop-tick

Stops authoritative tick loop for room. Client request:
  • Required:
    • type
  • Optional:
    • none Success response:

Query and projection commands

query.register

Client request:
  • Required:
    • type
    • query_spec_id
    • version
    • descriptor object
  • Optional:
    • descriptor fields depend on query implementation Success:
Reject:
  • query.register.rejected with reason_class: "reject.invalid_payload"

projection.build

Client request:
  • Required:
    • type
    • projection_id
    • query_spec_id
  • Optional:
    • target_checkpoint Success:
Reject classes include:
  • reject.invalid_payload
  • reject.query_spec_not_found
  • reject.checkpoint_selector_invalid
  • reject.checkpoint_not_found
  • reject.query_backpressure

projection.read

Client request:
  • Required:
    • type
    • projection_id
  • Optional:
    • limit
    • page_token Success:

projection.invalidate

Client request:
  • Required:
    • type
    • projection_id
  • Optional:
    • reason Success type: projection.invalidated

projection.list

Client request:
  • Required:
    • type
  • Optional:
    • query_spec_id
    • state_filter Success type: projection.list.result

replay.read-range

Client request:
  • Required:
    • type
    • key_prefix
  • Optional:
    • from_lamport
    • limit
    • cursor Success type: replay.read-range.result with items and next_cursor.

Archive control-plane commands

Archive commands serialize through shared ArchiveWsRequest and ArchiveWsResponse contracts from core/archive_contracts.rs.

Archive request schema summary

  • archive.describe
    • required: type, room, archive_ref
  • archive.validate
    • required: type, room, archive_ref, mode
  • archive.import
    • required: type, room, archive_ref, import_mode
    • optional: expected_checkpoint
  • archive.export
    • required: type, room, source_room, archive_ref

Archive response schema summary

  • archive.describe.result payload fields:
    • room, archive_ref, manifest_id, format_version, archive_kind, checkpoint, payload_digest_set
    • optional: compatibility_window, provenance
  • archive.validate.result payload fields:
    • room, archive_ref, accepted, mode, checks
    • optional: compatibility_window
  • archive.import.completed payload fields:
    • room, archive_ref, canonical_hash, checkpoint, imported_nodes, imported_blobs
  • archive.export.result payload fields:
    • room, source_room, archive_ref, manifest_id, checkpoint, payload_digest_set, compatibility_window
    • payload_digest_policy, policy_timeline_hash, policy_timeline_cutover_lamport, policy_timeline_transition_cutovers
  • Rejected payload shape (archive.*.rejected variants):
    • room, archive_ref, reason_class, reason_message

archive.describe

Client request:
Success type: archive.describe.result Canonical success envelope:
Reject path:
  • Handler-level rejection may surface as generic error ("<reason_class>: <reason_message>") for describe failures

archive.validate

Client request:
Success type: archive.validate.result
Reject type: archive.validate.rejected
Canonical success envelope:
Canonical reject envelope:

archive.import

Client request:
Success type: archive.import.completed
Reject type: archive.import.rejected
Canonical success envelope:
Canonical reject envelope:

archive.export

Client request:
Success type: archive.export.result
Reject type: archive.export.rejected
Canonical success envelope:
Canonical reject envelope:

Topology control-plane commands

Topology success responses serialize through shared TopologyWsResponse contracts from core/topology_contracts.rs.

Topology request schema summary

  • topology.create-child
    • required: type, child_room_id, child_purpose, promotion_policy_id, parent_checkpoint
    • optional: parent_room_id (defaults to current room), created_by
  • topology.describe-lineage
    • optional: room_id (defaults to current room)
  • topology.list-children
    • optional: parent_room_id (defaults to current room)
  • topology.propose-promotion
    • required: type, parent_room_id, child_room_id, child_checkpoint_hash, payload_ref
    • optional: idempotency_key
  • topology.validate-promotion
    • required: type, proposal_id
  • topology.apply-promotion
    • required: type, proposal_id

Topology response schema summary (success)

  • topology.create-child.completed payload:
    • child_room_id, lineage
  • topology.describe-lineage.result payload:
    • room_id, lineage, ancestors
  • topology.list-children.result payload:
    • parent_room_id, children
  • topology.propose-promotion.completed payload:
    • proposal_id, parent_room_id, child_room_id, child_checkpoint_hash, payload_ref, proposal_digest
  • topology.validate-promotion.completed payload:
    • proposal_id, validation_digest
  • topology.apply-promotion.completed payload:
    • proposal_id, parent_room_id, parent_new_canonical_hash, audit_key
Topology reject path:
  • Rejects are emitted as generic error messages ("<reason_class>: <reason_message>") from ingress handlers

topology.create-child

Client request:
Success type: topology.create-child.completed Canonical success envelope:
Canonical reject envelope (ingress error format):

topology.describe-lineage

Client request:
Success type: topology.describe-lineage.result Canonical success envelope:

topology.list-children

Client request:
Success type: topology.list-children.result Canonical success envelope:

topology.propose-promotion

Client request:
Success type: topology.propose-promotion.completed Canonical success envelope:
Canonical reject envelope (ingress error format):

topology.validate-promotion

Client request:
Success type: topology.validate-promotion.completed Canonical success envelope:

topology.apply-promotion

Client request:
Success type: topology.apply-promotion.completed Canonical success envelope:
Canonical reject envelope (ingress error format):

Compaction command

compact-room

Client request:
Server behavior:
  1. Compacts graph into snapshot
  2. Verifies snapshot metadata
  3. Rebuilds graph from snapshot
  4. Broadcasts snapshot-pack to room peers
  5. Replies to requester with compact-ack
Failure path returns error with compaction/verify/rebuild reason text.

Rejection taxonomy

Generic ingress errors

  • error envelope shape:
    • type: "error"
    • msg: "<reason>"
  • Common msg categories:
    • protocol/input validation (for example malformed pack)
    • auth (auth: ...)
    • capability denial (reject.control_plane_forbidden: ...)
    • archive/topology rejection surfaced as formatted text

Archive reason classes (ArchiveReasonClass)

  • reject.archive_unsupported_format
  • reject.archive_manifest_invalid
  • reject.archive_digest_mismatch
  • reject.archive_signature_invalid
  • reject.archive_checkpoint_not_found
  • reject.archive_policy_timeline_mismatch

Lineage reason classes (LineageReasonClass)

  • reject.lineage_parent_checkpoint_mismatch
  • reject.lineage_policy_unknown
  • reject.lineage_parent_not_found
  • reject.lineage_child_already_exists
  • reject.room_not_found
  • reject.lineage_invalid_checkpoint

Promotion reason classes (PromotionReasonClass)

  • reject.promotion_child_checkpoint_mismatch
  • reject.promotion_policy_denied
  • reject.promotion_invalid_lineage
  • reject.promotion_stale_parent
  • reject.promotion_apply_conflict
  • reject.promotion_not_found
  • reject.promotion_not_validated

Close-frame contract

Close codes used by server runtime:
  • 4001: resync required
  • 4002: token expired
  • 4008: rate limit exceeded
  • 1011: server overload
Use these codes in client reconnect/error handling policies.

Graph introspection (host-core direct)

These commands are available as HostCommand variants in nodalmerge-host-core for server integrators embedding the runtime directly (e.g., via nodalmerge-host-axum). They are not yet exposed as WebSocket commands. Frontend SDK developers encounter the effects of these commands when:
  • PromoteCheckpointToGraph runs and changes replay/canonical behavior
  • GetCanonicalResolution is used server-side to build projections or detect conflicts

PromoteCheckpointToGraph

Materializes a Canonical Checkpoint-plane snapshot into the room’s CRDT graph as a synthetic origin node. This is an explicit promotion boundary — after promotion, replay of the room’s graph always begins from the promoted node rather than original genesis. selector accepts the same checkpoint selector shape as projection.build:
Response event: CheckpointPromoted
This is idempotent: re-promoting the same checkpoint returns the existing node.

GetFrontier

Returns the current CRDT frontier — the set of leaf node IDs that have no known successors.
Response event: FrontierQueried
The frontier identifies the “head” of the room’s DAG. Useful for building custom sync-status UIs or validating that two peers have converged.

GetCausalParents

Returns the causal parent node IDs for a specific node in the sync graph.
Response event: CausalParentsQueried
Use this when building graph visualizers or debugging causal history for a specific change.

GetCanonicalResolution

Returns the conflict-resolved map state for the room. This is the authoritative view of all keys, post-conflict-resolution, at the current canonical checkpoint.
Response event: CanonicalResolutionQueried
Values are raw bytes (base64-encoded). Use this to build conflict inspector UIs or verify canonical state outside the normal sync path.

ComputeSyncDiff

Computes the set difference between the room’s current node set and a peer’s claimed node set. Returns which nodes only the server has and which the peer claims but the server lacks.
Response event: SyncDiffComputed
Use this in custom sync-status displays to show convergence progress or diagnose sync gaps.

InspectPack

Decodes a pack’s bytes and extracts causal metadata without applying it to any graph. Safe for debugging and validation workflows.
Response event: PackInspected
Use this to validate a pack before importing it, or to extract structural metadata for diagnostics.