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-presencereturns one row per live operator connection, includingdeviceId,roles, andscopes. 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.listincludes optionallastSeenAtMsandlastSeenReason. Connected nodes report current connection time with reasonconnect; 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:
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):
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:
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:
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
agentevents, tool-result events) require at leastoperator.read. Sessions without it skip these frames entirely. - Plugin-defined
plugin.*broadcasts are gated tooperator.writeoroperator.adminby default; explicit entries such asplugin.approval.requested/plugin.approval.resolveduseoperator.approvalsinstead. - Presence events require operator read access (
operator.read, also satisfied byoperator.writeoroperator.admin). Watched-session references are filtered for each recipient, including in hello snapshots andsystem-presencereplies. - Transport events such as
heartbeatandtickremain 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.