Gateway protocol versioning
Protocol version constants, the N-1 node window, and client defaults
Which protocol versions a client may negotiate, and the constants a reference client ships with.
Versioning
PROTOCOL_VERSION,MIN_CLIENT_PROTOCOL_VERSION,MIN_NODE_PROTOCOL_VERSION, andMIN_PROBE_PROTOCOL_VERSIONlive inpackages/gateway-protocol/src/version.ts.- Clients send
minProtocol+maxProtocol. Operator and UI clients must include the current protocol in that range; current clients and servers run protocol v4. - Authenticated clients with both
role: "node"andclient.mode: "node"may use the N-1 node protocol (v3). Lightweight restart checks use the same N-1 window. Device auth, pairing, scopes, command policy, and exec approvals are unchanged by this compatibility window. Plugin-owned node capabilities and commands are withheld until the node upgrades to the current protocol because their hosted surfaces are not part of the N-1 contract. - Schemas and models are generated from TypeBox definitions:
pnpm protocol:genpnpm protocol:gen:swiftpnpm protocol:check
Client constants
The reference client implementation lives in packages/gateway-client/src/
(OpenClaw wraps it via the thin src/gateway/client.ts facade). These
defaults are stable across protocol v4 and are the expected baseline for
third-party clients.
| Constant | Default | Source |
|---|---|---|
PROTOCOL_VERSION | 4 | packages/gateway-protocol/src/version.ts |
MIN_CLIENT_PROTOCOL_VERSION | 4 | packages/gateway-protocol/src/version.ts |
MIN_NODE_PROTOCOL_VERSION | 3 | packages/gateway-protocol/src/version.ts |
MIN_PROBE_PROTOCOL_VERSION | 3 | packages/gateway-protocol/src/version.ts |
| Request timeout (per RPC) | 30_000 ms | packages/gateway-client/src/client.ts (requestTimeoutMs) |
| Preauth / connect-challenge timeout | 15_000 ms | packages/gateway-client/src/timeouts.ts (OPENCLAW_HANDSHAKE_TIMEOUT_MS env can raise the paired server/client budget) |
| Initial reconnect backoff | 1_000 ms | packages/gateway-client/src/client.ts (GATEWAY_RECONNECT_POLICY) |
| Max reconnect backoff | 30_000 ms | packages/gateway-client/src/client.ts (GATEWAY_RECONNECT_POLICY) |
| Fast-retry clamp after device-token close | 250 ms | packages/gateway-client/src/client.ts |
Force-stop grace before terminate() | 250 ms | FORCE_STOP_TERMINATE_GRACE_MS |
stopAndWait() default timeout | 1_000 ms | STOP_AND_WAIT_TIMEOUT_MS |
Default tick interval (pre hello-ok) | 30_000 ms | packages/gateway-client/src/client.ts |
| Tick-timeout close | code 4000 when silence exceeds tickIntervalMs * 2 | packages/gateway-client/src/client.ts |
MAX_PAYLOAD_BYTES | 25 * 1024 * 1024 (25 MB) | src/gateway/server-constants.ts |
| Chat attachment ceiling | agents.defaults.mediaMaxMb, default 20 MB decoded | src/gateway/chat-attachment-policy.ts |
| Chat attachment image ceiling | min(attachment ceiling, 6 MB) | src/gateway/chat-attachment-policy.ts, packages/media-core/src/constants.ts |
The server advertises the effective policy.tickIntervalMs,
policy.maxPayload, policy.maxBufferedBytes, and policy.attachments in
hello-ok; clients should honor those values rather than the pre-handshake
defaults or hardcoded attachment sizes.
The reference client lets finite requests own their configured deadline when
every pending request has one. An expectFinal request without a finite
timeoutMs, any request with timeoutMs: null, or a mix of finite and
unbounded requests keeps the tick watchdog active. If inbound events and
responses remain silent past the tick-timeout threshold, the client closes the
socket with code 4000, rejects every pending request, and reconnects. It does
not replay rejected requests after reconnecting.
Local state owner routing
Supported local administrative commands discover the process that owns their selected physical state directory before opening mutation-capable state. A live owner receives authenticated RPCs on its recorded local port; a configured remote Gateway or URL override does not redirect these local targets. With no owner, the CLI acquires exclusive lifecycle ownership and retains it until accepted work and cleanup settle. An unreachable, uninspectable, or still-starting owner is not evidence that the directory is offline.
These routes require operator.admin and local-state-owner-routing-v1 in
hello-ok.features.capabilities. Method presence alone is insufficient. Except
for creation, which is covered by that original capability, each operation also
requires its own capability:
| Local operation | RPC | Additional capability |
|---|---|---|
worktrees create | worktrees.create | None |
worktrees remove, including lossless and exact-state modes | worktrees.remove | worktrees-remove-owner-v1 |
worktrees restore, including exact-state recovery | worktrees.restore | worktrees-restore-owner-v1 |
worktrees gc | worktrees.gc | worktrees-gc-owner-v1 |
worktrees recover-removal | worktrees.recoverRemoval | worktrees-recover-removal-owner-v1 |
worktrees retire-snapshot | worktrees.retireSnapshot | worktrees-retire-snapshot-owner-v1 |
pairing list | channels.pairing.list | channels-pairing-list-owner-v1 |
pairing approve | channels.pairing.approve | channels-pairing-approve-owner-v1 |
Default approvals get | exec.approvals.get | exec-approvals-get-owner-v1 |
Default approvals set and allowlist edits | exec.approvals.set | exec-approvals-set-owner-v1 |
The CLI sends the discovered expectedOwnerId; the server checks the current
owner and requester before effects. Worktree creation also carries
expectedRepoIdentity as the captured repository directory's device:inode.
These fields identify the intended owner and target; they grant no permission.
See worktree request and result contracts,
channel pairing,
and approval snapshots.
Refusals and uncertain outcomes
The CLI distinguishes OWNER_UNAVAILABLE (ownership cannot be established),
OWNER_REFUSED (dispatch did not occur or the server explicitly refused before
mutation), and OUTCOME_UNKNOWN (a dispatched operation may have taken effect).
A server error with details.mutationAccepted: false identifies the explicit
pre-mutation refusal; other errors after dispatch do not prove rollback.
None of these states triggers local fallback or automatic replay. For an older Gateway, missing capability, or authentication refusal, update the Gateway or fix authentication. To work offline, stop the Gateway through its service owner, wait for ownership to release, and rerun the exact command. After an uncertain reply, inspect the target and its operation-specific status or recovery output before deciding whether to retry. The local route allows up to ten minutes for the existing owner operation; a timeout does not transfer ownership.
Mixed versions and retained boundaries
New clients require the complete capability for their operation even if an older
Gateway advertises the same method. They never drop a selector or guard to make
an old request fit. Existing RPC clients can omit the additive owner fields on
methods that predate routing and retain their existing response contracts. The
new worktrees.recoverRemoval and worktrees.retireSnapshot methods require
expectedOwnerId.
Older CLI and SDK binaries keep their existing direct-write behavior. Routing does not intercept those writes, arbitrary trusted SQL, or another state root sharing an external database. Match CLI, Gateway, and SDK versions; callers must retain their existing foreign-commit freshness checks. Routing covers the named operations above; it does not guarantee that the Gateway is the only SQLite writer.
The following boundaries retain their existing owners and contracts:
| Boundary | Retained behavior |
|---|---|
| Device bootstrap | Implicit-loopback device-pairing recovery, QR/setup-code issuance, and SDK device tokens remain with the bootstrap owner so obtaining RPC credentials does not require those credentials first. |
| Configuration and agent setup | Agent identity, bindings, and config editing retain file-authoring/reload behavior. Interactive agent creation and onboarding still stage auth, plugins, workspace, and config locally; their authoritative commit has not moved to this route. |
| Exec-policy synchronization | exec-policy preset and exec-policy set retain their existing local config/policy owner. Routing the approvals commands does not change these operations or execution authorization. |
| Credentials | Model-auth keys/order and SDK auth writes, MCP OAuth, secret-store edits, and channel authentication retain their existing locking, invalidation, and lifecycle owners. |
| Client preferences | The TUI's remembered session remains a client-preference write, not conversation authority. |
| Plugin and delivery state | Plugin keyed stores and dedupe retain plugin ownership; delivery-queue operations retain outbound-owner admission. |
| Session and workspace SDKs | Session entry, maintenance, catalog/import/link, transcript/callback, and workspace/worktree APIs retain their existing contracts. Session cleanup, agent deletion, and foreign SDK workspace/worktree mutations are not covered by this routing contract. |
| Raw storage APIs | Raw cron replacement and low-level SQLite SDK/native access are not made exclusive by CLI routing. Prefer domain RPCs where available; a worker in a different process is still a foreign writer. |
| Offline and maintenance owners | sandbox recreate requires exclusive offline ownership. Embedded execution, reset/uninstall, boot admission, migrations, Doctor, and update retain their existing lifecycle owners. |
Updates do not require a new routing RPC to install the version that supplies it. The installed updater and candidate Doctor retain their stop, drain, backup, maintenance, and finalization contracts. This routing adds no protocol-version, schema, configuration, retention, or durability change.