On this page

On this page

Gateway

Gateway protocol transport

What the wire looks like before any method call: the published packages, the frame shapes, the payload limits, and the Gateway-controlled WebRTC Talk contract.

npm packages

Follow Install the packages for the verified stable release, exact-version commands, and compatibility guidance. Package release versions are separate from the wire protocol version and the root openclaw CLI release.

  • @openclaw/gateway-protocol publishes the schemas, validators, TypeScript types, lightweight frame and error helpers, and version constants. Its tarball includes the generated protocol.schema.json machine-readable contract as a downloadable file, not an exported import subpath.
  • @openclaw/gateway-client publishes the reference Node client and a browser-safe entry at @openclaw/gateway-client/browser.

For application lifecycle guidance, see Building a Gateway client. For apps that supervise the Gateway as a child process, see Embedding OpenClaw.

Transport and framing

  • WebSocket, text frames, JSON payloads.
  • First frame must be a connect request.
  • The default budget allows 128 outstanding unauthenticated connections per resolved client IP. Successful authentication or closure releases the slot. See pre-auth connection limits for shared-NAT behavior and the environment override.
  • Pre-connect frames are capped at 64 KiB (MAX_PREAUTH_PAYLOAD_BYTES). After handshake, follow hello-ok.policy.maxPayload and hello-ok.policy.maxBufferedBytes. With diagnostics enabled, oversized inbound frames and slow outbound buffers emit payload.large events before the gateway closes or drops the frame. These events carry surface, byte sizes, limits, and a safe reason code, never message bodies, attachment contents, raw frame bytes, tokens, cookies, or secrets.
  • The Gateway offers permessage-deflate to remote clients, with context takeover disabled in both directions. Direct-local connections skip compression to avoid extra RPC latency. Trusted reverse proxies and managed Tailscale ingress retain compression even when their Gateway-side socket is loopback. Plain TCP tunnels without proxy attribution appear direct-local and also skip compression. Peers that negotiate it (browsers, ws clients, Foundation, OkHttp) receive frames of 1 KiB and up compressed; smaller session and tool updates stay raw. Browsers compress every request; the Gateway inflates and deflates synchronously instead of through background zlib streams, so request bursts reach handler scheduling together, as uncompressed frames do. Large shared payloads are compressed once and reused across recipients; worker-produced history is compressed off the Gateway thread. Peers that do not offer the extension are unaffected. Payload limits apply to the inflated size.

Compression threshold

The 1 KiB threshold balances payload savings against synchronous deflate CPU at observed event rates. A synthetic corpus of 1,805 current producer frames (4,340,581 raw bytes), including session-row snapshots and deltas, presence resets and deltas, and agent, tool, observer, and chat events, produced these results with the shared-context level-1 codec and no context takeover:

Threshold Wire payload bytes Median main-thread CPU ms / 1,000 frames
256 B 1,357,907 21.884
512 B 1,402,623 18.019
1 KiB 1,464,046 16.986
2 KiB 2,334,915 8.011
4 KiB 3,078,930 4.002

At 1 KiB, this mix uses 52.45% fewer payload bytes than at 4 KiB, compared with 24.16% fewer at 2 KiB. The extra 12.984 ms of CPU per 1,000 frames costs about 0.32 ms/s at the observed aggregate rate of 25 event frames/s (about 180,000 over two hours), or 1.30 ms/s at a swarm peak of 100 frames/s: roughly 0.03–0.13% of one core. Below 1 KiB, savings flatten: 512 B saves only another 61,423 bytes (4.20%) because smaller frames compress less effectively without context takeover. The threshold decision uses cost at these event rates rather than a fixed CPU budget per 1,000 frames.

Measurements used an Apple M3 Ultra with Node 26.12.0, three warm-ups, and eight rotating measured rounds. Fresh segment identities prevented compressed-segment cache reuse from hiding deflate work. Corpus frequencies and text are synthetic; these savings are not a production egress forecast. Wire payload bytes exclude WebSocket, TLS, and TCP headers. See Prometheus metrics for per-event wire-byte counters and raw frame-size distributions that can validate the mix on a running Gateway.

Frame shapes:

  • Request: {type:"req", id, method, params, traceparent?, expectedProfileId?}
  • Response: {type:"res", id, ok, payload|error}
  • Event: {type:"event", event, payload, seq?, stateVersion?, recipientProfileId?}

Handler failures return a typed UNAVAILABLE response for the affected request and leave a healthy socket open. This includes connect preparation failures; a client whose connect failed must still complete a successful handshake before using the connection as an established session. An already admitted operator connection can continue to serve recovery RPCs even if its hello snapshot failed. Failures after hello delivery are logged without sending a second connect response. Malformed JSON closes with code 1007 and reason invalid JSON; handshake and authentication policy violations use their explicit policy close codes and reasons. A failed hello transport write closes with 1011 and reason gateway hello delivery failed.

Live text uses append deltas after an initial recipient snapshot. The chat.history field inFlightRun.text restores only the current run's unpersisted assistant tail. Silent-reply suppression uses the full available source; the tail keeps existing directive and media normalization without being classified as a silent reply again. If the cap evicts source context, retained tail text stays visible conservatively. Cumulative snapshots or replacements that restore text evicted by the live buffer cap may redisplay committed text when ownership is unknown, preserving unsaved output. When durable history or a keyed commentary item takes ownership of live text, the Gateway sends an existing replace: true frame before subsequent append deltas. Terminal result snapshots retain their complete result in message.content. A buffer-backed terminal can also supply the existing message.openclawDisplayContent projection: the same unpersisted tail finalized for display, including an explicitly empty tail and any Canvas blocks. The Control UI applies that projection before storing its live view; delivery consumers retain the complete content. An outer event sequence gap means a client may have lost part of that baseline: retire the connection and reconnect before applying more deltas. If the frame revealing the gap is a chat final, aborted, or error event, deliver its authoritative terminal outcome and supplied complete snapshot before gap callbacks retire the connection. This lets completed runs settle even when there will be no more live text to replay. Renew session subscriptions after reconnect; the Gateway sends a complete snapshot with the next text frame for each observed run. Run-local payload sequences can skip numbers because text is paced and coalesced; they are not the outer connection sequence.

Canonical agent events with stream: "item" and data.kind: "preamble" include optional preamble metadata. When the item replaces a live preview, preamble.retainedText supplies the producer's remaining answer projection, including an empty string when no answer prefix remains. An empty preamble object means this event carries no new retirement projection. Earlier preview credit can still cover later item IDs and completion echoes. Append-only clients can preserve a preview they already delivered, carry its credit across those identities, and rebase their final-answer state. Chat and raw assistant frames are paced independently; their arrival order and payload sequence numbers are not a reclassification identity. Older clients can ignore this additive metadata. ACP commentary reconciliation requires an updated Gateway and bridge.

Retirement uses assistant occurrence identity, never text-prefix matching. Producers correlate live occurrences with committed transcript idempotency keys or the host's exact source receipts. Native harnesses that persist part of a still-streaming item retain its text-merge itemId and separately identify each captured interval with occurrenceId. Unidentified text remains in the live tail until it gains an identity-bearing commit or the run terminates; equal durable text alone cannot hide it.

Clients that share one connection among several views must keep reconstruction with their local stream owner. A new local listener may join after the wire snapshot was delivered to another view. Seed it from that owner's current state, or deliver reconstructed local snapshots, and clear that state when its connection or run retires. Durable history alone is not an ordered live-text baseline.

After authentication, a client may include a W3C traceparent string on each request frame. The Gateway continues a valid value as a child trace context for that request. Missing or syntactically malformed values within the 128-character field limit keep the default fresh request trace and do not fail the RPC; longer values make the request frame invalid. The initial connect request never establishes trace context for later frames. Use a separate traceparent for each logical request on a long-lived connection; do not treat the WebSocket itself as one trace.

Response errors use { code, message, details?, retryable?, retryAfterMs? }. Authenticated operator requests share a bounded queue for starting RPC handlers. Ordinary requests have both aggregate and per-connection waiting limits, so concurrent Control UI setup can queue without giving one connection the entire request allowance. The aggregate serialized-byte bound still applies. Small sessions.messages.subscribe requests without approval replay and sessions.messages.unsubscribe requests have separate bounded waiting capacity, including a per-connection limit. All requests share the same yielding budget. Roster subscriptions, catalog reads, and message subscriptions with approval replay share four preparation slots. A slot remains occupied until its request settles, and each start yields to ready socket I/O. A request waiting for a preparation slot stays in the bounded queue while eligible requests start: chat.history and sessions.list reads can pass waiting preparations, as can ordinary requests on other connections. Other starts preserve FIFO order within each connection, including subscriptions and their later unsubscribe requests. WebSocket handshakes bypass the request queue, so reconnecting tabs cannot start an unbounded replay wave ahead of other clients' handshakes. When waiting capacity is exhausted, the Gateway returns retryable UNAVAILABLE before the method runs; retry within the request's budget. Starts still waiting after 30 seconds also return retryable UNAVAILABLE, with details.reason: "request-start-timeout". The deadline runs when the event loop can next make progress; it does not interrupt an already-started handler. Gateway RPC queue-wait diagnostics include the entire wait, including preparation capacity and timed-out starts. Started requests complete concurrently, so responses can arrive out of order.

During cooperative suspension, identity reads (agent.identity.get) wait in the shared browser/CLI client for gateway.suspension with phase accepting. A retryable UNAVAILABLE with details.reason: "gateway-suspending" also parks the read, using retryAfterMs (60 seconds) as a fallback if the resume event is missed. The original request deadline and cancellation still apply. Disconnects settle pending reads normally and use the existing reconnect backoff. Writes are neither parked nor replayed by this identity-read policy.

Ordinary UI/SDK requests may outlive a socket disconnect, but cannot start a handler in a retiring Gateway instance. Shutdown fences new request entry and joins pending handler loading and authorization before releasing their runtime. Already-started methods retain their own shutdown behavior; shutdown does not wait for every RPC to finish. Exact pending node progress and result replies remain available during node cleanup, until transport shutdown seals entry.

Clients should branch on code and details.code; message remains human-readable and can change except where a compatibility note says otherwise. Method-level authorization failures use top-level code: "FORBIDDEN" with structured missing-scope details:

  • Missing scope: { code: "MISSING_SCOPE", missingScope, requiredScopes }. requiredScopes is the complete known scope set for the requested operation. The legacy missing scope: <scope> message is retained for older clients.

Clients should read details first and use the legacy message only as a compatibility fallback. readMissingScopeError and readMissingScopeErrorDetails are exported from @openclaw/gateway-protocol/gateway-error-details; the browser-safe gateway client re-exports them from @openclaw/gateway-client/browser.

The schemas are exported as GatewayErrorDetailsSchema, MissingScopeErrorDetailsSchema from @openclaw/gateway-protocol/schema. HTTP scope failures mirror the MISSING_SCOPE object under error.details and use HTTP status 403.

Side-effecting methods require idempotency keys (see schema).

Profile binding

Use profile binding only when hello-ok.features.capabilities includes profile-binding-v1. A client that requires this contract must report it as unavailable when the capability is absent, rather than silently sending an unbound action. Requests that omit expectedProfileId retain existing behavior.

expectedProfileId is an optional opaque string of 1 to 128 characters on an authenticated request frame. The Gateway compares it exactly with the current primary profile ID of the authenticated principal. It does not trim, fold case, or follow merge aliases on the expected value. Obtain the ID from users.self; a Gateway URL, account label, agent ID, or session key is not a profile ID. A missing authenticated profile cannot satisfy the precondition.

A server advertising the capability checks the precondition at RPC entry and before returning a response payload. Commit-time revalidation is method-specific; the capability does not promise atomicity inside arbitrary methods or plugins. An entry check followed by asynchronous work is not itself a commit guarantee.

The structured error sets error.details.reason to EXPECTED_PROFILE_MISMATCH and carries a per-attempt classification in error.details.execution:

  • not_started: no execution was started by this attempt.
  • may_have_executed: the method may have run before the mismatch was detected; the error is not evidence of rollback.

Neither classification clears uncertainty from an earlier attempt or overrides a known acknowledgment (ACK). Preserve the original idempotency key and reconcile the earlier outcome before retrying uncertain work. Ordinary socket disconnects do not cancel already-accepted work.

On authenticated operator broadcasts, optional recipientProfileId identifies the recipient's primary profile at publication. It is a per-recipient frame fact, not the event's origin, a run-owner identity, or an authorization grant. Existing event permissions and subscriptions still govern delivery. Bind the consumer to both its physical Gateway connection and selected profile; if the recipient field is missing or differs, stop applying the event to that bound view or action and surface the binding failure.

This contract does not cover pre-authentication events, node event delivery, APNs notifications, or in-process publications. It does not revoke provider-direct media or already-issued WebRTC credentials, and it does not promise immediate revocation of retained sessions.

Connection keepalives

Authenticated control connections use WebSocket ping/pong keepalives. These are separate from scheduled agent heartbeats; disabling agent heartbeats does not disable connection monitoring.

A ping queued behind outgoing data is governed by transport inactivity, including partial write progress and incoming traffic. Once its write completes, the peer gets a full 25-second pong window; unrelated incoming messages do not extend that window. The periodic check closes expired connections and releases their owners. Transport inactivity is not an independent write-only deadline: a peer sending traffic can keep a queued write alive. Existing slow-consumer buffer limits still apply. Streaming transports retain their stream-owner lifecycle policy.

Gateway-controlled WebRTC Talk

talk.client.create accepts the additive capability gateway-control-v1. The released browser/Gateway-owned WebRTC route tries OAuth first and falls back to Platform API-key authentication. Direct backend sockets and unlisted or private realtime routes require Platform API-key authentication. A successful result includes clientControl: { owner: "gateway" }, a 60-second single-use Gateway broker token in clientSecret, and the relative offerUrl: "/plugins/openai/realtime/calls".

The client sends only application/sdp to that route with the broker token. It must not create a provider control data channel. The Gateway creates the call, attaches the provider sideband before returning the answer SDP, and owns tool, transcript, steering, cancellation, and close lifecycle. Clients that omit the capability retain the existing browser session behavior. A Gateway or configured authentication path that cannot provide the requested owner returns UNAVAILABLE; it never downgrades the request to client-owned control.

Clients must close their local media peer if the Gateway connection is lost or a talk.event for their current voiceSessionId contains talkEvent.type: "session.closed". Ignore terminal events for other calls; a recoverable session.error alone is not a close notification.

Was this useful?