Gateway core
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
openclaw.mjs— root entry, forwards todist/entry.jsafter the Node version check.entry.ts main:79-131—main()→runMainOrRootHelpdispatch.run-main gateway hot path:159-225—gateway rundynamic import.run-command .action:64-71— parses options then callsrunGatewayCommand.server.ts startGatewayServer:31-40— thin wrapper that lazy-loads server.impl.startGatewayServer implementation:551-580— actual startup: clean old plugin generations, bootstrap network runtime.coreGatewayHandlers:269— core RPC handler registry.createRequestGatewayMethodRegistry:598-630— merge core + plugin + extra handlers per request.handleGatewayRequest:638-660— authorize and dispatch a JSON-RPC request.createAgentEventHandler:299-320— broadcast agent events to WebSocket clients.
Data flow
The core startup sequence of startGatewayServer (server.impl.ts:551):
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:
/** 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:
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:
handleGatewayRequestfalls 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_DIRresolves 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.