openclaw/packages/gateway-client
Peter Steinberger 991b48e096
test(plugins,ui): remove low-value tests (batch d386) (#179465)
* test(groq): deslop z1041 tests

* test(groq): deslop z1041 tests

* test(ui): deslop z1040 tests

* test(ui): deslop z1040 tests

* test(ui): deslop z1040 tests

* test(line): deslop z1045 tests

* test(logs): deslop z1048 tests

* test(llm-task): deslop z1047 tests

* test(llm-task): deslop z1047 tests

* test(node-cli): deslop z1043 tests

* test(node-cli): deslop z1043 tests

* test(xai): deslop z1049 tests

* test(gateway-client): deslop z1050 tests

* test(agent-guidance): deslop z1044 tests

* test(agent-guidance): deslop z1044 tests

* test(agent-guidance): deslop z1044 tests

* test(ai): deslop z1052 tests

* test(deepgram): deslop z1051 tests

* test(tts-local-cli): deslop z1053 tests

* test(channels): deslop z1054 tests

* test(imessage): deslop z1056 tests

* test(agents): deslop z1057 tests

* test(buzz): deslop z1055 tests

* test(buzz): deslop z1055 tests

* test(canvas): deslop z1059 tests

* test(whatsapp): deslop z1046 tests

* test(whatsapp): deslop z1046 tests

* test(whatsapp): deslop z1046 tests

* test(file-transfer): deslop z1058 tests

* test: preserve fixture types and configured group coverage

* test: fix catalog fixtures and preserve mock isolation

* test(canvas): explain renderer mock isolation

* test: prune obsolete mock export baseline entries
2026-10-21 13:08:47 +00:00
..
src test(plugins,ui): remove low-value tests (batch d386) (#179465) 2026-10-21 13:08:47 +00:00
CHANGELOG.md build(client): make @openclaw/gateway-client publishable to npm (#111707) 2026-07-20 00:23:36 -07:00
LICENSE build(client): make @openclaw/gateway-client publishable to npm (#111707) 2026-07-20 00:23:36 -07:00
package.json chore(release): close out 2026.10.5 on main (#176607) 2026-10-18 19:15:02 -07:00
README.md fix(state): messages to agents fail right after a Gateway restart instead of waiting (#174958) 2026-10-19 11:39:17 +00:00

@openclaw/gateway-client

Reference WebSocket client for the OpenClaw Gateway protocol. It provides the connection state machine used by OpenClaw's own Node and browser clients: challenge-based authentication, typed protocol frames, request correlation, timeouts, reconnect backoff, device-token handling, and event delivery.

The current wire protocol is version 4. General clients must advertise exactly v4 with minProtocol: 4 and maxProtocol: 4. See the Gateway protocol specification for the complete handshake, authentication, role, scope, and method contracts. Exact node identities (role: "node" plus mode: "node") and diagnostic clients can use v3. The built-in node host starts with an exact v4 envelope, then retries an exact v3 envelope after a v3 Gateway rejects v4. If that legacy connection attempt reaches an upgraded v4 Gateway, the client reconnects with the full v4 envelope before reporting readiness. Other exact node identities default to [3, 4]. Explicit bounds override these defaults; [3, 4] on the built-in node host selects the same bounded negotiation.

Versioning

Package versions follow the OpenClaw calendar release train: YYYY.M.PATCH, including the OpenClaw prerelease suffix when applicable. The package version is separate from the Gateway's current wire protocol number reported in hello-ok.

Install

Use the verified stable release with exact pins:

npm install --save-exact @openclaw/gateway-client@2026.8.1 @openclaw/gateway-protocol@2026.8.1

See the installation guide for package/wire-version rules and recovery from reserved 0.0.0 artifacts. Test it with the Gateway version you deploy; the root openclaw CLI has its own package versions and dist-tags.

This release declares Node.js >=22.19.0. Node consumers use the ws transport included as a runtime dependency. Browser consumers provide their platform WebSocket through the browser-safe protocol client surface.

For device-authenticated Node connections, supply deviceIdentity (or hostDeps.loadOrCreateDeviceIdentity) and the hostDeps.signDevicePayload and hostDeps.publicKeyRawBase64UrlFromPem callbacks. The host also owns device-token storage through GatewayClientHostDeps; the package does not load OpenClaw's local identity or credentials automatically.

Token storage callbacks may return their existing synchronous result or a Promise. The client waits for token loading before sending connect, and for issued-token persistence before calling onHelloOk. An accepted hello creates a persistence obligation that survives disconnect or stop; bootstrap credentials retire after that persistence succeeds. Readiness still belongs to the current connection. stop() requests shutdown synchronously; stopAndWait() also waits for accepted token operations to settle, even when transport closure times out. A reconnect waits for earlier token operations before reading the token again.

Accepted asynchronous persistence failures reach onConnectError even after the connection retires. If that callback is absent or throws, stopAndWait() rejects with the first undelivered persistence error after draining accepted work. A later connection can still load credentials; a reported failure does not poison its storage queue. Synchronous callback exceptions retain their existing connect-error behavior.

Hosts should honor the optional expectedToken storage condition. A string compares the existing row before writing or clearing; null on a store means insert only when no row exists. Omission preserves unconditional storage behavior. This prevents an older receipt or cleanup from replacing another client's newer token. Close cleanup waits for pending persistence and, if its result is uncertain, conditionally clears only the sampled and received tokens. Cleanup before any token observation retains its existing unconditional behavior. An observed empty cache does not permit unconditional cleanup.

When storage callbacks receive signal or assertCurrent, check them immediately before admission and before committing a write. These callbacks stay local to the host; do not send them to a worker. Loads use the current connection lifetime; cleanup uses the client lifetime. Accepted hello persistence is independent of transport lifetime and retains the host's normal storage admission checks.

Entry points

  • @openclaw/gateway-client exports the Node GatewayClient, device-auth helpers, readiness helpers, and timeout utilities.
  • @openclaw/gateway-client/browser exports the browser-safe protocol client, browser device-auth lifecycle, reconnect policy, and lightweight protocol constants. Its module graph does not import Node built-ins or ws.
  • @openclaw/gateway-client/readiness exports helpers that delay client startup until the event loop can process Gateway IO.
  • @openclaw/gateway-client/timeouts exports timeout constants and safe timer resolution helpers.
  • @openclaw/gateway-client/websocket-data converts every Node ws raw-data shape to UTF-8 text.

Node quickstart

import { GatewayClient } from "@openclaw/gateway-client";
import { PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version";

const connected = Promise.withResolvers<void>();
const client = new GatewayClient({
  url: "ws://127.0.0.1:18789",
  token: process.env.OPENCLAW_GATEWAY_TOKEN,
  minProtocol: PROTOCOL_VERSION, // v4
  maxProtocol: PROTOCOL_VERSION, // v4
  onHelloOk: () => connected.resolve(),
  onConnectError: (error) => connected.reject(error),
  onEvent: (event) => {
    console.log(event.event, event.payload);
  },
});

client.start();
await connected.promise;

const status = await client.request("status", {});
console.log(status);

client.stop();

The client waits for the Gateway's connect.challenge event before sending its connect request. It includes the challenge nonce in device authentication and does not fall back to a pre-challenge handshake. onHelloOk fires only after the Gateway accepts a compatible connection, so requests should wait for that callback.

This loopback example uses the default gateway-client / backend identity. It is not a device-pairing example. UI clients should declare their actual mode and supply the device-auth host callbacks described above; see device identity and pairing.

For remote connections, prefer wss://. The Node client also accepts plaintext ws:// by default for loopback, private/link-local/CGNAT IP addresses, and .local or .ts.net hostnames. This allowlist does not provide encryption: authentication material and Gateway traffic must not cross an untrusted network without transport security.

Browser clients

Import @openclaw/gateway-client/browser when the host owns the WebSocket adapter and device-key storage. The browser entry includes GatewayProtocolClient and GatewayBrowserDeviceAuthLifecycle; it deliberately omits the Node transport, TLS fingerprint handling, and private-network address policy.

The host is responsible for:

  • creating a GatewayProtocolSocket adapter around the browser WebSocket;
  • loading and storing browser device identity and issued device tokens;
  • signing the challenge-bound device payload;
  • using the Gateway challenge ts as the device proof's signedAt value;
  • supplying the client identity, role, scopes, and authentication selection;
  • choosing close and reconnect behavior for product-specific errors.

The shared protocol client still owns frame parsing, request correlation, challenge ordering, timeout cleanup, sequence-gap detection, and reconnect scheduling.

Both the Node and browser entries export isGatewayProtocolResponseError(error). It recognizes correlated Gateway response errors; constructed errors and local transport timeouts return false. Both entries use the same implementation.

Defaults and reconnect behavior

The Node client starts with a 30 second request timeout, a 30 second connect-challenge timeout, and exponential reconnect backoff from 1 second to 30 seconds with a multiplier of 2. Reconnects use randomized delays: the first waits 1–1.2 seconds, and sustained failures spread retries across 25–30 seconds. Retryable server hints remain minimum waits and can extend beyond the normal cap, with up to 20% additional spread. Adapter-owned startup retry hints retain their exact next delay without advancing normal backoff.

Browser adapters can observe the shared client's onReconnectScheduled(delayMs, signal) callback to display the actual retry wait, including jitter and timer bounds. The signal aborts when the wait is canceled or superseded; adapters should clear their countdown when creating the next socket.

A sequence gap calls onGap and retires the socket unless the callback already replaced it. A gap-revealing chat final, aborted, or error event is delivered first so its authoritative outcome can settle the run. Other gapped frames and subsequent frames from that socket are not delivered. Reconnect restores a fresh live-text baseline; applications should also refresh durable state and restore their session subscriptions.

Session subscription requests time out without compensating RPCs. If the Gateway applied a subscribe or unsubscribe but its acknowledgment was lost, observation may remain stale until a fresh subscription or reconnect. In particular, a retained handle may miss updates after an unacknowledged unsubscribe. The caller still receives the timeout; ordinary acknowledged leases and release retries retain their ownership and ordering.

The defaults table and the server policy fields that can replace pre-handshake values are documented in the Gateway protocol specification.

Use the ./timeouts entry point when a host must align readiness or watchdog budgets with these defaults. Use the ./readiness entry point when startup must wait for an event-loop check before opening the socket.

Streaming chat

The runId returned by chat.send identifies the submitted input even when a steer falls back to a followup. Queue admission is nonterminal: keep observing that ID until the consuming execution completes, or the input is rejected, canceled, or dropped before consumption. Collected inputs each receive the batch execution's terminal outcome under their original IDs. SDK run handles use the same IDs for events, wait(), and cancellation; canceling an input before consumption withdraws only that input. A steer accepted into the active turn continues to complete after its transcript receipt while that turn remains active.

Event callbacks receive the wire payload unchanged. A chat delta's optional message is an authoritative snapshot that already includes deltaText. Without message, append deltaText to the run's existing text. replace: true replaces the text, including an empty replacement. The first frame received for a run and frames that change canvas or media content supply a snapshot.

Both public entries export mergeChatStreamMessage(previousMessage, payload) for clients that own their run state. The helper preserves nontext content and message metadata and never appends a snapshot's delta twice. An append without a baseline returns undefined; recover the connection instead of displaying an incomplete answer. reduceSessionProjectionRunEvent uses the same merge operation for clients using the shared session projection.

The high-level @openclaw/sdk retains reconstructed chat and assistant-item text in its normalized run-event replay, so late readers can recover the text after the initial wire snapshot is evicted. Its rawEvents() and each normalized event's raw field still expose the original wire event, including omitted message fields. Normalized assistant events retain cumulative data.text for their current item; data.delta keeps its wire meaning, and the raw event can omit data.text. Active chat baselines remain protected while their connection is current. The SDK's concrete Gateway transport reconciles outstanding owned or observed runs after reconnect through agent.wait. Active chat runs can recover display text from an exact chat.history.inFlightRun match; newer live text always wins over an older history response. Assistant-item text re-baselines on its next wire snapshot because history contains display text, not raw assistant-item text.

SDK and ACP recovery share recoverTerminalReply: it collects all assistant items for the run in transcript order, preserves live item boundaries, and reads full messages when history truncated them. Recovered text preserves leading indentation and trailing whitespace. It scans at most ten 200-record pages; an incomplete scan reports unavailable instead of presenting a partial reply. The bounded terminalReply.text summary is never used as complete output. Local normalized recovery events have no raw field. Their data.recovery.status reports rebaselined, recovered, or unavailable; unavailable full text omits outputText. A bare wait timeout is not a run terminal. Terminal observations have finite retention, and sessionless or unavailable transcript occurrences cannot be reconstructed; unknown outcomes remain unsettled with a recovery notice. Automatic recovery stops after four consecutive unavailable observations (initial check plus three waits), releases retention protection, and stays stopped across reconnects. Confirmed activity resets that budget; late terminal frames can still settle the run. Queued replies use exponential backoff with the Gateway client's positive 20% jitter and a 25–30 second cap. ACP reports unavailable full text explicitly instead of settling with a partial answer. Closing the client or returning a run iterator cancels its recovery work.

Reconnect retires old text baselines. Ending the transport event stream also releases outstanding-run protection, retaining only bounded replay. Custom OpenClawTransport implementations must deliver terminal outcomes or end a retired event stream; the generic transport interface does not expose reconnect notifications and does not receive this automatic reconciliation. Confirmed session unsubscribe also releases that session's baseline protection; it also releases unobserved runs accepted before their first output. The acknowledgment cannot retire a newer acceptance or subscription's snapshot. The concrete Gateway transport preserves acknowledgment and event order. Custom transports must preserve that ordering or end their retired event stream. For raw global or unknown session keys, pass agentId when unsubscribing to identify an agent-owned baseline. An unscoped acknowledgment cannot retire a baseline with a known agent owner.

Bundled internals

The retry supervisor and the small @openclaw/net-policy/ip implementation are inlined into the published JavaScript and declarations. They are implementation details, not public exports or supported API surfaces. ipaddr.js remains an external dependency because the inlined IP helpers use its public runtime and types.

ws, @openclaw/gateway-protocol, and ipaddr.js remain external in the published distribution. Consumers should import protocol types and constants from @openclaw/gateway-protocol, not from bundled implementation paths.

Contract notes

  • The client is inert at module import and construction time. start() opens the socket; stop() closes it and rejects pending requests.
  • A request uses request(method, params) after hello-ok. Passing timeoutMs: null creates an intentionally unbounded request.
  • signal normally stops observing a request immediately on abort. A write caller that must reconcile a sent request can opt into retainSentResultOnAbort: true: abort still cancels unsent work and retries, while a sent request retains its response and original deadline. A later pre-dispatch refusal rejects as aborted instead of retrying.
  • Finite request deadlines reject with GatewayProtocolRequestTimeoutError, whose CLIENT_TIMEOUT code, method, deadline, and send-boundary flag remain distinct from authoritative Gateway response errors.
  • Device identity persistence, signing, proxy routing, TLS formatting, and logging stay host-owned through GatewayClientHostDeps.
  • Protocol changes are additive first. Incompatible changes require an explicit wire-version decision and coordinated server/client follow-through.