On this page

On this page

Gateway

Gateway protocol presence and events

How the Gateway reports which nodes are connected, and which server-pushed events reach a given session.

Presence

  • system-presence returns one row per live operator connection, including deviceId, roles, and scopes. Separate tabs keep their own watched-session facts. Reconnecting removes offline rows for the same identity: device ID, then client instance ID, then connection ID. When the last connection closes, at most one offline row for that identity remains until the five-minute TTL. Node connections retain device-keyed rows. UIs can group rows by device or person.
  • node.list includes optional lastSeenAtMs and lastSeenReason. Connected nodes report current connection time with reason connect; paired nodes can also report durable background presence via a trusted node event.

Opt-in presence deltas

Operator clients can advertise presence-deltas in connect caps. The Control UI and current macOS, iOS, and Android operator connections do so. Older app versions and clients without the capability retain the existing presence event payload { presence: [...] }. Ordinary system-presence calls still return an array.

For opted-in readers, the same presence event carries one of two payloads:

json
{  "reset": true,  "version": 12,  "presence": [{ "connectionId": "connection-a", "ts": 1000 }],  "keys": ["1"]}
json
{  "base": 12,  "version": 13,  "upsert": [{ "key": "1", "set": { "ts": 2000 }, "unset": ["watchedSessions"] }],  "remove": [],  "order": ["1"]}

Keys are opaque, stable identities assigned by the presence owner, independent of display names and recipient visibility. A reset replaces the complete roster; keys are compact identifiers, not connection or device IDs, and are never reused within a Gateway process after a row expires or is removed. Do not persist or parse them. keys parallels presence. For an existing row, an upsert's set contains only changed top-level fields and unset names removed fields. Nested objects and arrays are replaced as fields, not merged. A new row uses { key, presence } with its complete contents. A field patch requires an existing row; otherwise recover with a snapshot. Apply removals and upserts, then select rows in the complete order array. This preserves the full snapshot's ordering even when only one row changes. All rows are projected for that recipient before comparison, including watched session visibility changes.

Heartbeat and activity timestamps are included in patches, so freshness remains identical to a full snapshot without retransmitting each connection's metadata.

version uses stateVersion.presence. Apply a delta only when base matches the last accepted version. On mismatch, request system-presence with { "snapshot": true }; an opted-in client receives the reset shape above. Ordinary calls still return the existing array. The Control UI reconciles these updates in its Gateway store so downstream consumers receive full rosters. The shared macOS/iOS channel reconstructs rosters before bounded subscriber queues, and reconnects to resynchronize if a version or transport sequence gap occurs. It also accepts full snapshots from older Gateways. Android currently ignores roster events, so its operator sockets opt in without maintaining a roster. Node-only connections do not receive presence events. Native changes take effect with app updates; a Gateway restart does not upgrade connected app code.

The hello snapshot remains snapshot.presence with snapshot.stateVersion.presence. This implementation sends a reset on the first post-hello presence event instead of assuming the hello roster is a recorded delivery baseline. Reconnects start the same way. A reset is also sent when there is no baseline, after a recovery RPC, or when encoding a delta would not save bytes.

Presence remains best effort with dropIfSlow. Each connection retains only references to its last sent projected rows and their version. A dropped frame invalidates that baseline, so the next presence event is a reset. The baseline advances only after the socket accepts a frame; send failures and disconnects clear it. Ordered WebSocket delivery makes successfully sent deltas composable; this is not a peer acknowledgment or a durable delivery guarantee.

Native macOS nodes can also send authenticated node.presence.activity events with bounded input idle time and an optional source: app for interaction with OpenClaw or system for system-wide physical activity. App-local reports do not require Accessibility; system-wide reports do. An omitted source retains the legacy system-wide permission requirement. The Gateway derives activity timestamps on its own clock, exposes the freshest connected Mac through node.list and node.describe, and broadcasts node.presence updates to read-scoped clients.

The app sends { "action": "clear" } when system-wide detection is disabled, then reports any observed app-local activity. When Accessibility is lost, it falls back to app-local activity or clears the sample if none has been observed. The Gateway clears timestamps only for that exact authenticated node connection. Gateways that predate this acknowledged action return it as unhandled, so the Mac node reconnects once and lets disconnect cleanup remove the old connection state. See Active computer presence for selection, privacy, model context, and notification-routing behavior.

Node host stats

Connected CLI node hosts and the macOS app's shared node-host worker send a resource snapshot immediately after connecting, then every 60 seconds. They call node.event with event: "node.host.stats" and an object payload (or its JSON encoding in payloadJSON):

json
{  "event": "node.host.stats",  "payload": {    "cpuCount": 8,    "loadAverage": [1.25, 1.1, 0.9],    "memoryTotalBytes": 17179869184,    "memoryFreeBytes": 4294967296,    "diskTotalBytes": 1000000000000,    "diskAvailableBytes": 250000000000  }}

cpuCount is an integer from 1 to 4096. Optional loadAverage contains the 1-, 5-, and 15-minute averages, each finite and between 0 and 100000. Windows has no load average; hosts omit the field when all three readings are zero. Memory and disk values are non-negative integer bytes, with free or available bytes no greater than their total. On macOS, memoryFreeBytes includes reclaimable host memory, so idle file cache is not reported as used; other platforms retain host-wide free-memory readings. Disk fields appear together only when the host can read capacity for the volume containing its home directory, independent of the worker's current directory.

The Gateway accepts updates only from the current node connection and stamps updatedAtMs with its own receipt time; nodes never send a timestamp. Successful updates appear as hostStats in node.list and node.describe and broadcast node.hostStats with { nodeId, hostStats } to read-scoped operators, using dropIfSlow: true. Stats are operator-facing and do not update model-visible node context. When received, the Gateway persists the snapshot as lastHostStats on the paired node record. Disconnecting or reconnecting without a new snapshot leaves the previous value intact. node.list and node.describe use live session stats while connected and project the saved snapshot as hostStats while offline, keeping its original updatedAtMs so clients can show the last-known age.

The structured node.event result uses reason: "updated", "stale_connection", or "invalid_payload". An older Gateway may return handled: false; the node continues at the normal cadence without an immediate retry.

Node background alive event

Nodes call node.event with event: "node.presence.alive" to record that a paired node was alive during a background wake, without marking it connected:

json
{  "event": "node.presence.alive",  "payloadJSON": "{\"trigger\":\"silent_push\",\"sentAtMs\":1737264000000,\"displayName\":\"Peter's iPhone\",\"version\":\"2026.4.28\",\"platform\":\"iOS 18.4.0\",\"deviceFamily\":\"iPhone\",\"modelIdentifier\":\"iPhone17,1\",\"pushTransport\":\"relay\"}"}

trigger is a closed enum: background, silent_push, bg_app_refresh, significant_location, manual, connect. Unknown values normalize to background (src/shared/node-presence.ts). The event only persists for authenticated node device sessions; device-less or unpaired sessions return handled: false.

Successful gateways return a structured result:

json
{  "ok": true,  "event": "node.presence.alive",  "handled": true,  "reason": "persisted"}

Older gateways may return only { "ok": true } for node.event; treat that as an acknowledged RPC, not durable presence persistence.

Broadcast event scoping

Server-pushed broadcast events are scope-gated so pairing-scoped or node-only sessions do not passively receive session content (src/gateway/server-broadcast.ts):

  • Chat, agent, and tool-result frames (streamed agent events, tool-result events) require at least operator.read. Sessions without it skip these frames entirely.
  • Plugin-defined plugin.* broadcasts are gated to operator.write or operator.admin by default; explicit entries such as plugin.approval.requested / plugin.approval.resolved use operator.approvals instead.
  • Presence events require operator read access (operator.read, also satisfied by operator.write or operator.admin). Watched-session references are filtered for each recipient, including in hello snapshots and system-presence replies.
  • Transport events such as heartbeat and tick remain available to every authenticated session.
  • Unknown broadcast event families are blocked unless the caller has the required scope unless a registered handler explicitly relaxes them.

Each client connection keeps its own per-client sequence number, so broadcasts stay monotonically ordered on that socket even when different clients see different scope-filtered subsets of the event stream.

hello-ok.features.capabilities advertises additive wire contracts. Native clients send sessionKey in chat.metadata only when session-scoped-chat-metadata is present; otherwise they retain the agent-only request supported by stable v2026.7.1-2. That older response describes agent-wide availability, not a session's selected profile. Retire this negotiation only when the minimum supported Gateway contract guarantees session-scoped metadata. Method or event presence alone is insufficient.

Was this useful?