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/synchronizationfor lifecycle flowprotocol/blob-flowfor transfer behavior
Endpoint and handshake
- Route:
GET /ws/:room_id(WebSocket upgrade) - First client frame must be a valid
hellopayload - 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,capspeer_id— logical peer identity, stable across reconnects. Falls back topubkeyif 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_typeare echoed inpeer-joined/peer-leftbroadcasts to other room members, and let the server targetpackbroadcasts at a specific logical peer across reconnects rather than a single socket session- On join, the server sends a full-state
packcatch-up (an exported snapshot of the room’s node history) immediately afterwelcome, distinct from the diff-based catch-up described inprotocol/synchronization
welcome (server -> client)
Envelope fields (from shared WelcomeEnvelope):
- Required:
type: "welcome"rootfrontiermissingcapsserver_pubkeypeers
- Optional:
mst_root
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
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:
typenodes(base64-encoded postcard node pack)
- Success emits:
packrelay to peers with fields fromPeerPackRelayEnvelope(type,from,nodes,root)
- Reject/error emits:
errorwithmsg = "invalid pack: bad base64"errorwithmsg = "invalid pack: bad postcard"- close
4008on peer rate-limit abuse
pack-ack (server -> client)
Acknowledges a submitted pack, reporting how many of its nodes were accepted.
accepted_count < incoming_countorrejected_count > 0indicates 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; seesdk/javascriptfor client-side handling.
request
Request:
- Required:
type
- Optional:
known(defaults to empty set behavior when omitted) Response (ServerPackReplyEnvelope):
mst-request
Request:
- Required:
type
- Optional:
pathsResponse:
mst-done
Request:
-
Required:
type
-
Optional:
missing_idsResponse (when IDs present):
-
pack(type,from: "server",nodes,root)
Blob commands
blob-upload
Request:
- Required:
typeblobs[]withhashanddata_b64
- Success emits:
blob-availablebroadcast withhashes
blob-request
Request:
- Required:
typehashes[]
- Success emits one or both:
blob-redirectwithredirects[](hash,url,expires_at_unix)blob-packwithblobs[](hash,data) andrequested[]
request-upload
Request:
- Required:
typehashsize
- Optional:
content_type
- Success emits:
upload-granted(hash,url,expires_at_unix)- or
upload-denied(hash,reason)
- Reject/error emits:
errorfor unsupported direct blob I/O negotiation or invalid hash
blob-uploaded
Request:
- Required:
typehash
- Success emits:
blob-availablebroadcast
- Reject emits:
upload-rejected(hash,reason) when verify fails
Presence and relay
presence
Request:
- Required:
typedata
- Success emits:
presencerelay with fields fromPresenceEnvelope(type,from,data)
- Behavior:
- Presence is ephemeral and not persisted in room DAG
WebRTC relay messages
Client commands:webrtc-offerwebrtc-answerwebrtc-ice
- Normalizes relay payload
- Adds
from - Broadcasts to room; receiver selection is client-side via
to
Control-plane capability gates
Denied control-plane commands return:set-policy->policy.adminset-room-key->room.adminstart-tick,stop-tick->tick.adminarchive.describe->archive.readarchive.validate,archive.import,archive.export->archive.adminquery.register,projection.build,projection.invalidate->query.adminprojection.read,projection.list,replay.read-range->query.readtopology.*->topology.admin
Room, policy, and tick admin commands
set-room-key
Locks an open room with an Ed25519 verifying key.
Client request:
- Required:
typepubkey
- Optional:
- none Success response:
error.msg = "reject.control_plane_forbidden: command=set-room-key requires=room.admin"errorwith set-room-key-specific reason message for invalid key or already-locked room
set-policy
Installs room write policy.
Client request:
- Required:
typedefault(allowordeny)rules[]withpath_glob
- Optional:
can_writeentries per rule Success response:
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:
typequery_spec_idversiondescriptorobject
- Optional:
- descriptor fields depend on query implementation Success:
query.register.rejectedwithreason_class: "reject.invalid_payload"
projection.build
Client request:
- Required:
typeprojection_idquery_spec_id
- Optional:
target_checkpointSuccess:
reject.invalid_payloadreject.query_spec_not_foundreject.checkpoint_selector_invalidreject.checkpoint_not_foundreject.query_backpressure
projection.read
Client request:
- Required:
typeprojection_id
- Optional:
limitpage_tokenSuccess:
projection.invalidate
Client request:
- Required:
typeprojection_id
- Optional:
reasonSuccess type:projection.invalidated
projection.list
Client request:
- Required:
type
- Optional:
query_spec_idstate_filterSuccess type:projection.list.result
replay.read-range
Client request:
- Required:
typekey_prefix
- Optional:
from_lamportlimitcursorSuccess type:replay.read-range.resultwithitemsandnext_cursor.
Archive control-plane commands
Archive commands serialize through sharedArchiveWsRequest and ArchiveWsResponse contracts from core/archive_contracts.rs.
Archive request schema summary
archive.describe- required:
type,room,archive_ref
- required:
archive.validate- required:
type,room,archive_ref,mode
- required:
archive.import- required:
type,room,archive_ref,import_mode - optional:
expected_checkpoint
- required:
archive.export- required:
type,room,source_room,archive_ref
- required:
Archive response schema summary
archive.describe.resultpayload fields:room,archive_ref,manifest_id,format_version,archive_kind,checkpoint,payload_digest_set- optional:
compatibility_window,provenance
archive.validate.resultpayload fields:room,archive_ref,accepted,mode,checks- optional:
compatibility_window
archive.import.completedpayload fields:room,archive_ref,canonical_hash,checkpoint,imported_nodes,imported_blobs
archive.export.resultpayload fields:room,source_room,archive_ref,manifest_id,checkpoint,payload_digest_set,compatibility_windowpayload_digest_policy,policy_timeline_hash,policy_timeline_cutover_lamport,policy_timeline_transition_cutovers
- Rejected payload shape (
archive.*.rejectedvariants):room,archive_ref,reason_class,reason_message
archive.describe
Client request:
archive.describe.result
Canonical success envelope:
- Handler-level rejection may surface as generic
error("<reason_class>: <reason_message>") for describe failures
archive.validate
Client request:
archive.validate.resultReject type:
archive.validate.rejected
Canonical success envelope:
archive.import
Client request:
archive.import.completedReject type:
archive.import.rejected
Canonical success envelope:
archive.export
Client request:
archive.export.resultReject type:
archive.export.rejected
Canonical success envelope:
Topology control-plane commands
Topology success responses serialize through sharedTopologyWsResponse 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
- required:
topology.describe-lineage- optional:
room_id(defaults to current room)
- optional:
topology.list-children- optional:
parent_room_id(defaults to current room)
- optional:
topology.propose-promotion- required:
type,parent_room_id,child_room_id,child_checkpoint_hash,payload_ref - optional:
idempotency_key
- required:
topology.validate-promotion- required:
type,proposal_id
- required:
topology.apply-promotion- required:
type,proposal_id
- required:
Topology response schema summary (success)
topology.create-child.completedpayload:child_room_id,lineage
topology.describe-lineage.resultpayload:room_id,lineage,ancestors
topology.list-children.resultpayload:parent_room_id,children
topology.propose-promotion.completedpayload:proposal_id,parent_room_id,child_room_id,child_checkpoint_hash,payload_ref,proposal_digest
topology.validate-promotion.completedpayload:proposal_id,validation_digest
topology.apply-promotion.completedpayload:proposal_id,parent_room_id,parent_new_canonical_hash,audit_key
- Rejects are emitted as generic
errormessages ("<reason_class>: <reason_message>") from ingress handlers
topology.create-child
Client request:
topology.create-child.completed
Canonical success envelope:
error format):
topology.describe-lineage
Client request:
topology.describe-lineage.result
Canonical success envelope:
topology.list-children
Client request:
topology.list-children.result
Canonical success envelope:
topology.propose-promotion
Client request:
topology.propose-promotion.completed
Canonical success envelope:
error format):
topology.validate-promotion
Client request:
topology.validate-promotion.completed
Canonical success envelope:
topology.apply-promotion
Client request:
topology.apply-promotion.completed
Canonical success envelope:
error format):
Compaction command
compact-room
Client request:
- Compacts graph into snapshot
- Verifies snapshot metadata
- Rebuilds graph from snapshot
- Broadcasts
snapshot-packto room peers - Replies to requester with
compact-ack
error with compaction/verify/rebuild reason text.
Rejection taxonomy
Generic ingress errors
errorenvelope shape:type: "error"msg: "<reason>"
- Common
msgcategories:- 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_formatreject.archive_manifest_invalidreject.archive_digest_mismatchreject.archive_signature_invalidreject.archive_checkpoint_not_foundreject.archive_policy_timeline_mismatch
Lineage reason classes (LineageReasonClass)
reject.lineage_parent_checkpoint_mismatchreject.lineage_policy_unknownreject.lineage_parent_not_foundreject.lineage_child_already_existsreject.room_not_foundreject.lineage_invalid_checkpoint
Promotion reason classes (PromotionReasonClass)
reject.promotion_child_checkpoint_mismatchreject.promotion_policy_deniedreject.promotion_invalid_lineagereject.promotion_stale_parentreject.promotion_apply_conflictreject.promotion_not_foundreject.promotion_not_validated
Close-frame contract
Close codes used by server runtime:4001: resync required4002: token expired4008: rate limit exceeded1011: server overload
Graph introspection (host-core direct)
These commands are available asHostCommand 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:
PromoteCheckpointToGraphruns and changes replay/canonical behaviorGetCanonicalResolutionis 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:
CheckpointPromoted
GetFrontier
Returns the current CRDT frontier — the set of leaf node IDs that have no known successors.
FrontierQueried
GetCausalParents
Returns the causal parent node IDs for a specific node in the sync graph.
CausalParentsQueried
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.
CanonicalResolutionQueried
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.
SyncDiffComputed
InspectPack
Decodes a pack’s bytes and extracts causal metadata without applying it to any graph. Safe for debugging and validation workflows.
PackInspected
Source-of-truth pointers
nodalmerge/server/src/ws_handler.rsnodalmerge/server/src/adapter_context.rsnodalmerge/server/src/query_control.rsnodalmerge/core/src/archive_contracts.rsnodalmerge/core/src/room_lineage.rsnodalmerge/core/src/topology_contracts.rsnodalmerge/host-core/src/protocol.rsnodalmerge/docs/operations-inventory.mdprotocol/websocket-messages