# Gateway protocol transport

> Gateway WS transport: packages, frame shapes, limits, and WebRTC Talk control

- 网址：https://funcoding.ai/agents/openclaw/gateway/protocol/transport/
- 来源：OpenClaw 官方文档原文（英文），MIT 许可，同步于 2026-10-11
- 官方原文：https://docs.openclaw.ai/zh-CN/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](https://funcoding.ai/agents/openclaw/gateway/clients/#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`](https://www.npmjs.com/package/@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`](https://unpkg.com/@openclaw/gateway-protocol@2026.8.1/protocol.schema.json)
  machine-readable contract as a downloadable file, not an exported import subpath.
- [`@openclaw/gateway-client`](https://www.npmjs.com/package/@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](https://funcoding.ai/agents/openclaw/gateway/clients/). For apps
that supervise the Gateway as a child process, see
[Embedding OpenClaw](https://funcoding.ai/agents/openclaw/gateway/embedding/).

## 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](https://funcoding.ai/agents/openclaw/gateway/security/rate-limiting/#unauthenticated-websocket-connections)
  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 does not negotiate `permessage-deflate`. Browsers can compress
  even tiny requests when the extension is enabled; serial decompression then
  delays each request behind busy event-loop turns before handler scheduling.
  Uncompressed frames preserve responsive request bursts at the cost of more
  bandwidth for large histories and rosters. Payload limits are unchanged.

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?}`

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
canonical 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 canonical 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](https://funcoding.ai/agents/openclaw/gateway/heartbeat/); 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.
