Skip to content

Gateway core ​

源码版本v2026.6.11

Responsibilities ​

startGatewayServer is the entry point of OpenClaw's resident process: it binds a port, loads plugins, registers RPC methods, and broadcasts agent events. All channel messages, ACP requests, and web clients connect to this gateway; it forwards requests to the agent main loop and broadcasts agent events back to each client.

The gateway itself doesn't "think" — it only receives, routes, broadcasts: it takes JSON-RPC requests, routes them to handlers (many of which land on the agent), and broadcasts agent events to the WebSocket connections subscribed to that session.

Design motivation ​

Why isolate the gateway as a resident process? Because OpenClaw plugs into 22 channels, and most of them need long-lived connections (WebSocket / polling). If every message started a short-lived process, the handshake overhead and state-recovery complexity would overwhelm the design. A resident gateway lets every channel connection, every agent session, and every plugin state live in one process — cross-layer coordination (e.g. a message on channel A triggers an agent tool call, and the tool result must be delivered to channel B) becomes just an in-process function call.

Key files ​

Data flow ​

The core startup sequence of startGatewayServer (server.impl.ts:551):

typescript
export async function startGatewayServer(
  port = 18789,
  opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
  normalizeStateDirEnv(process.env);
  // Clean up old plugin generations left from the previous restart, so state is clean between restarts
  const installRecords = loadInstalledPluginIndexInstallRecordsSync();
  const removedGenerations = await cleanupRetiredManagedNpmInstallGenerations({
    activeInstallPaths: Object.values(installRecords).flatMap((record) =>
      record.installPath ? [record.installPath] : [],
    ),
    onError: (error, projectRoot) =>
      log.warn(`failed to clean retained npm generation ${projectRoot}: ${String(error)}`),
  });
  const { bootstrapGatewayNetworkRuntime } = await import("./server-network-runtime.js");
  bootstrapGatewayNetworkRuntime();

Note the default port 18789, and that the first thing startup does is clean up old plugin npm generations — this is designed for in-process restart: when the gateway restarts, it first reclaims plugin resources from the previous round to avoid leaks.

Request dispatch (handleGatewayRequest:638) goes over JSON-RPC:

typescript
/** Authorizes and dispatches one gateway JSON-RPC-style request. */
export async function handleGatewayRequest(
  opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers },
): Promise<void> {
  const { req, respond, client, isWebchatConnect, context } = opts;
  // Prefer the caller-attached registry (if it owns the method); otherwise rebuild from the live plugin registry
  // so plugin RPC methods registered after the startup snapshot stay reachable (#94127)

The handler registry is rebuilt per request (createRequestGatewayMethodRegistry:598): core handlers are merged with currently-active plugin handlers and any caller-provided extra handlers. This design lets plugins hot-register RPC methods without restarting the gateway.

Agent event broadcast (createAgentEventHandler:299) injects three primitives:

typescript
export function createAgentEventHandler({
  broadcast,              // broadcast to all connections subscribed to this session
  broadcastToConnIds,     // targeted broadcast to specific connections
  nodeSendToSession,      // cross-node delivery to a session
  agentRunSeq,
  chatRunState,
  resolveSessionKeyForRun,
  ...

broadcast and broadcastToConnIds are injected by createGatewayNodeSessionRuntime in server.impl.ts:971, threading through the gateway lifecycle.

Boundaries and failures ​

  • in-process restart: on restart, the gateway first cleans old plugin npm generations (src/gateway/server.impl.ts:560). If cleanup fails, it only warns and doesn't block startup — bringing the service up matters more than a clean cleanup.
  • plugin hot-registration handler reachability: handleGatewayRequest falls back to the live registry when the caller's snapshot doesn't own the method, fixing #94127 — otherwise plugin RPCs registered after the startup snapshot would be invisible.
  • state directory: startup first calls normalizeStateDirEnv(process.env) to ensure $OPENCLAW_STATE_DIR resolves correctly, otherwise config and workspace paths would be wrong.
  • network runtime: bootstrapGatewayNetworkRuntime() is called before the network layer loads; wrong order would leave WebSocket channels unready.

Summary ​

The gateway only does three things — receive, route, broadcast — but each is designed for "resident + multi-channel + hot-pluggable": clean on startup, rebuild handlers per request, and inject broadcast primitives. What actually "thinks" is the Agent main loop; see RPC method table for how requests get authorized and dispatched, and Chat broadcast for how events reach clients.

Official references: Gateway docs · README.