<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Agent Internals</title>
    <description>The latest articles on DEV Community by Agent Internals (@agentinternals).</description>
    <link>https://dev.to/agentinternals</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3806909%2Fd269ae2b-9bf7-4f2a-a33a-02a3f232ef3b.png</url>
      <title>DEV Community: Agent Internals</title>
      <link>https://dev.to/agentinternals</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agentinternals"/>
    <language>en</language>
    <item>
      <title>How OpenClaw Serves HTTP, WebSocket, and 70+ Methods on a Single Port</title>
      <dc:creator>Agent Internals</dc:creator>
      <pubDate>Thu, 05 Mar 2026 02:40:25 +0000</pubDate>
      <link>https://dev.to/agentinternals/how-openclaw-serves-http-websocket-and-70-methods-on-a-single-port-4g10</link>
      <guid>https://dev.to/agentinternals/how-openclaw-serves-http-websocket-and-70-methods-on-a-single-port-4g10</guid>
      <description>&lt;p&gt;&lt;strong&gt;From HTTP/WebSocket multi-protocol multiplexing to configuration hot-reload — a deep breakdown of how the Gateway starts and runs&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Version baseline: OpenClaw 2026.3.2&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Gateway serves HTTP and WebSocket traffic on a single port (18789) with no reverse proxy required, using Node.js's native &lt;code&gt;upgrade&lt;/code&gt; event to multiplex protocols.&lt;/li&gt;
&lt;li&gt;Startup is a precisely ordered 48-step, 9-phase sequence -- from config migration and secrets resolution through to sidecar lifecycle management -- where each phase has clear failure semantics.&lt;/li&gt;
&lt;li&gt;HTTP routing uses a stage pipeline with first-match-wins semantics: 13 ordered stages cover webhooks, OpenAI-compatible APIs, Canvas, plugins, and health probes, with zero overhead for disabled features.&lt;/li&gt;
&lt;li&gt;Configuration hot-reload supports 4 modes (off/restart/hot/hybrid), with a recursive deep-diff engine and per-subsystem reload rules that allow most changes to take effect without a full Gateway restart.&lt;/li&gt;
&lt;li&gt;Slow consumer protection on WebSocket broadcasts prevents a single laggy client from causing unbounded memory growth -- low-priority events are dropped, critical events force-disconnect the slow client.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Gateway Overview: Single Process, Single Port, Multi-Protocol&lt;/li&gt;
&lt;li&gt;Startup Sequence in Detail&lt;/li&gt;
&lt;li&gt;HTTP Routing Architecture&lt;/li&gt;
&lt;li&gt;WebSocket Protocol&lt;/li&gt;
&lt;li&gt;Configuration Hot-Reload&lt;/li&gt;
&lt;li&gt;Health Monitoring and Presence&lt;/li&gt;
&lt;li&gt;Global Architecture Overview&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Gateway Overview: Single Process, Single Port, Multi-Protocol
&lt;/h2&gt;

&lt;p&gt;The OpenClaw Gateway is the central hub of the entire system. It serves externally as a &lt;strong&gt;single process on a single port&lt;/strong&gt;, carrying both HTTP and WebSocket protocols simultaneously, and routing multiple higher-level functional modules (Control UI, OpenAI-compatible API, Webhooks, plugins, Canvas, etc.) over the same connection infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Default Port and Bind Modes
&lt;/h3&gt;

&lt;p&gt;The Gateway listens on port &lt;strong&gt;18789&lt;/strong&gt; by default. The port can be overridden at startup via the &lt;code&gt;--port&lt;/code&gt; parameter or the &lt;code&gt;OPENCLAW_GATEWAY_PORT&lt;/code&gt; environment variable. &lt;code&gt;startGatewayServer()&lt;/code&gt; writes this variable immediately on startup (&lt;code&gt;src/gateway/server.impl.ts:240&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/server.impl.ts:232-240&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startGatewayServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18789&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GatewayServerOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GatewayServer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="c1"&gt;// Ensure all default port derivations (browser/canvas) see the actual runtime port.&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENCLAW_GATEWAY_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bind mode is specified via the &lt;code&gt;GatewayServerOptions.bind&lt;/code&gt; field (or &lt;code&gt;gateway.bind&lt;/code&gt; in &lt;code&gt;openclaw.json&lt;/code&gt;), with four options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bind Mode&lt;/th&gt;
&lt;th&gt;Bind Address&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loopback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local single-device, secure default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.0.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sharing across devices on a LAN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tailnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tailscale IPv4 (&lt;code&gt;100.64.0.0/10&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Exposing via Tailscale network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prefers loopback, falls back to LAN&lt;/td&gt;
&lt;td&gt;Auto-detection, suitable for general deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the bind mode is non-loopback, &lt;code&gt;createGatewayRuntimeState()&lt;/code&gt; proactively prints a security warning in the logs, prompting the user to ensure authentication is configured (&lt;code&gt;src/gateway/server-runtime-state.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️  Gateway is binding to a non-loopback address.
    Ensure authentication is configured before exposing to public networks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common CLI commands for starting the Gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start with default port (loopback)&lt;/span&gt;
openclaw gateway run

&lt;span class="c"&gt;# Bind to LAN, allowing local network connections&lt;/span&gt;
openclaw gateway run &lt;span class="nt"&gt;--bind&lt;/span&gt; lan &lt;span class="nt"&gt;--port&lt;/span&gt; 18789

&lt;span class="c"&gt;# Run in the background (daemon mode)&lt;/span&gt;
&lt;span class="nb"&gt;nohup &lt;/span&gt;openclaw gateway run &lt;span class="nt"&gt;--bind&lt;/span&gt; loopback &lt;span class="nt"&gt;--port&lt;/span&gt; 18789 &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/openclaw-gateway.log 2&amp;gt;&amp;amp;1 &amp;amp;

&lt;span class="c"&gt;# Check channel status (with probe)&lt;/span&gt;
openclaw channels status &lt;span class="nt"&gt;--probe&lt;/span&gt;

&lt;span class="c"&gt;# Check health endpoint&lt;/span&gt;
curl http://127.0.0.1:18789/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How HTTP and WebSocket Share a Single Port
&lt;/h3&gt;

&lt;p&gt;Node.js's &lt;code&gt;http.Server&lt;/code&gt; emits an &lt;code&gt;upgrade&lt;/code&gt; event to handle HTTP-to-WebSocket protocol upgrade requests. The Gateway listens for this event in &lt;code&gt;attachGatewayUpgradeHandler()&lt;/code&gt; (&lt;code&gt;src/gateway/server-http.ts&lt;/code&gt;, line 650), forwarding requests with the &lt;code&gt;Upgrade: websocket&lt;/code&gt; header to the &lt;code&gt;WebSocketServer&lt;/code&gt; (&lt;code&gt;ws&lt;/code&gt; library), while regular HTTP requests are handled by the &lt;code&gt;handleRequest()&lt;/code&gt; function. This allows HTTP and WebSocket traffic to naturally multiplex over the same port without requiring an additional reverse proxy.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Startup Sequence in Detail
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;startGatewayServer()&lt;/code&gt; function is defined at &lt;code&gt;src/gateway/server.impl.ts:232&lt;/code&gt; and serves as the Gateway's main entry point. The entire startup process comprises approximately 48 steps, organized into nine phases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Source location&lt;/strong&gt;: &lt;code&gt;src/gateway/server.impl.ts&lt;/code&gt; is approximately 1100 lines long, starting from the function signature at line 232 through to the end of the file where the &lt;code&gt;GatewayServer&lt;/code&gt; object is returned. Core startup logic is concentrated in lines 250-750.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Startup Sequence ASCII Diagram
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startGatewayServer(port=18789, opts)
│
├─ Phase 1: Config Pipeline
│   ├─ readConfigFileSnapshot()          Read raw openclaw.json snapshot
│   ├─ migrateLegacyConfig()             Detect legacy fields and auto-migrate
│   ├─ writeConfigFile()                 Write migration results back to disk
│   ├─ applyPluginAutoEnable()           Auto-enable qualifying plugins
│   └─ loadConfig()                      Load the final effective configuration
│
├─ Phase 2: Auth &amp;amp; Secrets
│   ├─ prepareSecretsRuntimeSnapshot()   Resolve SecretRefs, validate availability
│   ├─ ensureGatewayStartupAuth()        Ensure auth token is generated/saved
│   ├─ activateSecretsRuntimeSnapshot()  Activate secrets runtime snapshot
│   └─ maybeSeedControlUiAllowedOrigins  Seed CORS allowlist for non-loopback installs
│
├─ Phase 3: Registry &amp;amp; Methods
│   ├─ initSubagentRegistry()            Initialize subagent registry
│   ├─ listGatewayMethods()              Enumerate 73 core WS methods
│   ├─ loadGatewayPlugins()              Load plugins, merge plugin methods into gatewayMethods
│   └─ listChannelPlugins()              List built-in channel plugin gatewayMethods
│
├─ Phase 4: HTTP / WebSocket
│   ├─ loadGatewayTlsRuntime()           Load TLS certificates (optional)
│   ├─ createGatewayRuntimeState()       → {
│   │    ├─ createGatewayHttpServer()    Build HTTP(S) server (Node http/https)
│   │    ├─ WebSocketServer({ maxPayload: 25MB })
│   │    ├─ listenGatewayHttpServer()    Bind port and start accepting connections
│   │    └─ createGatewayBroadcaster()   Broadcaster (scope filtering + slow-consumer protection)
│   │   }
│   └─ attachGatewayUpgradeHandler()     Attach HTTP→WS upgrade handler
│
├─ Phase 5: Services
│   ├─ new NodeRegistry()                Node (Pi/mobile/desktop) registry
│   ├─ buildGatewayCronService()         Scheduled task service
│   ├─ createChannelManager()            Unified channel manager (Slack/Telegram/Discord...)
│   └─ startGatewayDiscovery()           mDNS/wide-area discovery broadcast
│
├─ Phase 6: Maintenance Timers
│   └─ startGatewayMaintenanceTimers()   → {
│        ├─ tick:    broadcast "tick" every 30s (keepalive)
│        ├─ health:  refreshGatewayHealthSnapshot() every 60s
│        └─ dedupe:  clean dedup cache + timeout-abort chat runs every 60s
│       }
│
├─ Phase 7: Event System
│   ├─ onAgentEvent(createAgentEventHandler())  Subscribe to agent runtime events
│   ├─ onHeartbeatEvent()                       Subscribe to heartbeat events → broadcast
│   └─ startHeartbeatRunner()                   Start heartbeat timer loop
│
├─ Phase 8: Sidecars
│   ├─ startGatewayTailscaleExposure()   Tailscale exposure (optional)
│   ├─ startGatewaySidecars() → {
│   │    ├─ startBrowserControlServer()  Browser control (optional)
│   │    ├─ startGmailWatcher()          Gmail hook watcher (optional)
│   │    ├─ startChannels()              Start all configured channels
│   │    └─ pluginServices.start()       Plugin service lifecycle
│   │   }
│   └─ hookRunner.runGatewayStart()      Fire gateway_start plugin hook
│
└─ Phase 9: Config Reloader
    └─ startGatewayConfigReloader()      chokidar watches openclaw.json
         ├─ stabilityThreshold: 200ms
         └─ debounce: 300ms (configurable)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Function Details
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — &lt;code&gt;readConfigFileSnapshot()&lt;/code&gt;&lt;/strong&gt;: Returns a &lt;code&gt;ConfigFileSnapshot&lt;/code&gt; containing the raw JSON, Zod validation result, and migration detection result. If the configuration file is invalid, &lt;code&gt;startGatewayServer()&lt;/code&gt; throws an exception with an error message that includes the specific field path and prompts the user to run &lt;code&gt;openclaw doctor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — &lt;code&gt;activateRuntimeSecrets()&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;src/gateway/server.impl.ts:318-362&lt;/code&gt;): Uses a Promise chain (&lt;code&gt;secretsActivationTail&lt;/code&gt;) internally to implement a serialization lock, ensuring that secrets activation does not race under concurrent hot-reload scenarios. If secrets resolution fails, the runtime retains the "last-known-good" snapshot and notifies the user via the &lt;code&gt;SECRETS_RELOADER_DEGRADED&lt;/code&gt; system event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/server.impl.ts:310-316 — Promise chain serialization lock&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runWithSecretsActivationLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;secretsActivationTail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;secretsActivationTail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 3 — &lt;code&gt;loadGatewayPlugins()&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;src/gateway/server.impl.ts:428-444&lt;/code&gt;): Loads all enabled plugins from disk, injects built-in handlers via &lt;code&gt;coreGatewayHandlers&lt;/code&gt;, and merges plugin-provided &lt;code&gt;gatewayMethods&lt;/code&gt; into the &lt;code&gt;gatewayMethods&lt;/code&gt; array. Deduplication is performed via &lt;code&gt;Array.from(new Set([...]))&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/server.impl.ts:443-444&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channelMethods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listChannelPlugins&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayMethods&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gatewayMethods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;baseGatewayMethods&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;channelMethods&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 4 — &lt;code&gt;WebSocketServer({ maxPayload })&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;maxPayload&lt;/code&gt; is set to &lt;code&gt;MAX_PAYLOAD_BYTES = 25 * 1024 * 1024&lt;/code&gt; (25 MB), consistent with the client, to prevent unexpected disconnections during high-resolution Canvas snapshot transfers (see comments in &lt;code&gt;src/gateway/server-constants.ts&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 8 — &lt;code&gt;startGatewaySidecars()&lt;/code&gt;&lt;/strong&gt;: Defined in &lt;code&gt;src/gateway/server-startup.ts&lt;/code&gt;, this function manages the lifecycle of "sidecar" processes including Browser Control, Gmail Watcher, channel startup, and plugin services. When the Gateway shuts down, all sidecars are stopped in an orderly fashion.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. HTTP Routing Architecture
&lt;/h2&gt;

&lt;p&gt;The HTTP request processing entry point is the internal function &lt;code&gt;handleRequest()&lt;/code&gt; within &lt;code&gt;createGatewayHttpServer()&lt;/code&gt;, defined at &lt;code&gt;src/gateway/server-http.ts&lt;/code&gt; line 486.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Philosophy: Stage Pipeline, First Match Wins
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;handleRequest()&lt;/code&gt; wraps all routes into a &lt;code&gt;GatewayHttpRequestStage[]&lt;/code&gt; array, trying each stage in sequence. Any stage returning &lt;code&gt;true&lt;/code&gt; means "handled" and terminates the pipeline. The advantage of this design is that route priority is entirely determined by array order, eliminating the need for complex route-table matching logic and making priority changes easy to track in diffs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP request arrives
│
├─ [Stage 1]  hooks          POST /hooks/*  → Webhook entry point (with rate limiting)
├─ [Stage 2]  tools-invoke   POST /v1/tools/invoke → Direct tool invocation
├─ [Stage 3]  slack          Slack Events API / Interactive Components
├─ [Stage 4]  openresponses  POST /v1/responses (requires enabled=true)
├─ [Stage 5]  openai         POST /v1/chat/completions (requires enabled=true)
├─ [Stage 6]  canvas-auth    Canvas path auth pre-check
├─ [Stage 7]  a2ui           Canvas A2UI assets (/a2ui/*)
├─ [Stage 8]  canvas-http    Canvas WebSocket proxy + static assets
├─ [Stage 9]  plugin-auth    Plugin route auth pre-check
├─ [Stage 10] plugin-http    Plugin-registered custom HTTP routes
├─ [Stage 11] control-ui-avatar  /avatar/* proxy (controlUiEnabled only)
├─ [Stage 12] control-ui-http    Control UI SPA catch-all (controlUiEnabled only)
└─ [Stage 13] gateway-probes GET /health /healthz /ready /readyz
     └─ No match → 404 Not Found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage's &lt;code&gt;run()&lt;/code&gt; returns &lt;code&gt;boolean | Promise&amp;lt;boolean&amp;gt;&lt;/code&gt;, where &lt;code&gt;true&lt;/code&gt; indicates the request has been consumed. &lt;code&gt;runGatewayHttpRequestStages()&lt;/code&gt; sequentially &lt;code&gt;await&lt;/code&gt;s each stage and returns upon encountering &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Probe Endpoints
&lt;/h3&gt;

&lt;p&gt;Four health probe paths are handled uniformly by &lt;code&gt;handleGatewayProbeRequest()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /health   → { ok: true, status: "live" }
GET /healthz  → { ok: true, status: "live" }
GET /ready    → { ok: true, status: "ready" }
GET /readyz   → { ok: true, status: "ready" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;HEAD&lt;/code&gt; methods are accepted; other methods return 405. Response headers include &lt;code&gt;Cache-Control: no-store&lt;/code&gt;. These endpoints &lt;strong&gt;require no authentication&lt;/strong&gt; and follow the standard Kubernetes liveness/readiness probe format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook Hook Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Defined at the top of &lt;code&gt;src/gateway/server-http.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOOK_AUTH_FAILURE_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOOK_AUTH_FAILURE_WINDOW_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means more than 20 authentication failures within 60 seconds will trigger rate limiting for that client IP. The Hook rate limiter is scoped via &lt;code&gt;AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH&lt;/code&gt;, isolated from the WS authentication rate limiter, so the two do not interfere with each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  openclaw.json Configuration Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loopback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"chatCompletions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After enabling the OpenAI-compatible API via &lt;code&gt;openclaw config set gateway.http.endpoints.chatCompletions.enabled true&lt;/code&gt;, Stage 5 (&lt;code&gt;openai&lt;/code&gt;) is added to the pipeline. If not enabled, the stage is never added to the array — zero overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. WebSocket Protocol
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Frame Type Definitions
&lt;/h3&gt;

&lt;p&gt;The WebSocket frame schema is defined in &lt;code&gt;src/gateway/protocol/schema/frames.ts&lt;/code&gt;, built with &lt;code&gt;@sinclair/typebox&lt;/code&gt;. There are three top-level frame types, discriminated by the &lt;code&gt;type&lt;/code&gt; field (discriminated union):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame Type&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;req&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client → Server&lt;/td&gt;
&lt;td&gt;Method call request, carries &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;params&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;res&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server → Client&lt;/td&gt;
&lt;td&gt;Method response, carries &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;payload&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;event&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server → Client&lt;/td&gt;
&lt;td&gt;Server broadcast, carries &lt;code&gt;event&lt;/code&gt;, &lt;code&gt;payload&lt;/code&gt;, &lt;code&gt;seq&lt;/code&gt;, &lt;code&gt;stateVersion&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RequestFrameSchema (src/gateway/protocol/schema/frames.ts)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;req&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ResponseFrameSchema&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;res&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ErrorShape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// EventFrameSchema&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;StateVersion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;seq&lt;/code&gt; field in &lt;code&gt;event&lt;/code&gt; frames is a monotonically increasing global sequence number (maintained by &lt;code&gt;createGatewayBroadcaster()&lt;/code&gt;), allowing clients to detect dropped frames. &lt;code&gt;stateVersion&lt;/code&gt; contains &lt;code&gt;presence&lt;/code&gt; and &lt;code&gt;health&lt;/code&gt; version numbers, enabling clients to determine whether they need to refresh their local cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Constants (src/gateway/server-constants.ts)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_PAYLOAD_BYTES&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 25 MB, max single-frame payload&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFERED_BYTES&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 50 MB, per-connection send buffer cap&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_HANDSHAKE_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Handshake timeout: 10 seconds&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TICK_INTERVAL_MS&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// Keepalive tick: every 30 seconds&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HEALTH_REFRESH_INTERVAL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Health snapshot refresh: every 60 seconds&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEDUPE_TTL_MS&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Dedup TTL: 5 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connection Handshake Sequence
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;attachGatewayWsConnectionHandler()&lt;/code&gt; (&lt;code&gt;src/gateway/server/ws-connection.ts&lt;/code&gt;, line 93) is the main handler for WebSocket connections. The handshake flow is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Gateway (server)
  │                                   │
  │──── TCP/TLS + HTTP Upgrade ──────&amp;gt;│
  │                                   │ wss.on("connection")
  │                                   │ connId = randomUUID()
  │                                   │ handshakeTimer = setTimeout(10s)
  │&amp;lt;─── event: connect.challenge ─────│  { nonce, ts }
  │                                   │
  │──── req: connect ────────────────&amp;gt;│  {
  │       (minProtocol, maxProtocol,  │    auth.token / auth.password,
  │        client info, auth, caps)   │    device / scopes
  │                                   │  }
  │                                   │ Validate token / rate limiter check
  │                                   │ Negotiate protocol version
  │                                   │ clearTimeout(handshakeTimer)
  │&amp;lt;─── res: hello-ok ────────────────│  {
  │       (protocol, server.connId,   │    features.methods[73+],
  │        features, snapshot,        │    features.events[],
  │        policy, canvasHostUrl)     │    policy.maxPayload,
  │                                   │    policy.tickIntervalMs,
  │                                   │    snapshot (presence+health)
  │                                   │  }
  │                                   │ clients.add(client)
  │                                   │ handshakeState = "connected"
  │                                   │
  │&amp;lt;══════ Normal bidirectional communication ═══════│
  │                                   │
  │   Every 30s:                      │
  │&amp;lt;─── event: tick ──────────────────│  { ts } (dropIfSlow=true)
  │                                   │
  │──── req: {method} ───────────────&amp;gt;│
  │&amp;lt;─── res: {id, ok, payload} ───────│
  │                                   │
  │&amp;lt;─── event: agent / health / ... ──│  (broadcast)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a successful handshake, the server lists all available methods and events in the &lt;code&gt;features&lt;/code&gt; field of &lt;code&gt;hello-ok&lt;/code&gt;. Clients should use this as the source of truth and should not hard-code method names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method Dispatch
&lt;/h3&gt;

&lt;p&gt;The Gateway core has &lt;strong&gt;100 built-in methods&lt;/strong&gt; (including those contributed by channel plugins). The BASE_METHODS array is defined in &lt;code&gt;src/gateway/server-methods-list.ts&lt;/code&gt;, covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session management&lt;/strong&gt;: &lt;code&gt;sessions.list&lt;/code&gt;, &lt;code&gt;sessions.preview&lt;/code&gt;, &lt;code&gt;sessions.reset&lt;/code&gt;, &lt;code&gt;sessions.compact&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel operations&lt;/strong&gt;: &lt;code&gt;channels.status&lt;/code&gt;, &lt;code&gt;channels.logout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent control&lt;/strong&gt;: &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;agents.list&lt;/code&gt;, &lt;code&gt;agents.create&lt;/code&gt;, &lt;code&gt;agents.update&lt;/code&gt;, &lt;code&gt;agents.delete&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models/tools&lt;/strong&gt;: &lt;code&gt;models.list&lt;/code&gt;, &lt;code&gt;tools.catalog&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled tasks&lt;/strong&gt;: &lt;code&gt;cron.list&lt;/code&gt;, &lt;code&gt;cron.add&lt;/code&gt;, &lt;code&gt;cron.run&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node pairing&lt;/strong&gt;: &lt;code&gt;node.pair.request&lt;/code&gt;, &lt;code&gt;node.invoke&lt;/code&gt;, &lt;code&gt;node.list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device auth&lt;/strong&gt;: &lt;code&gt;device.pair.approve&lt;/code&gt;, &lt;code&gt;device.token.rotate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat&lt;/strong&gt;: &lt;code&gt;chat.send&lt;/code&gt;, &lt;code&gt;chat.abort&lt;/code&gt;, &lt;code&gt;chat.history&lt;/code&gt; (WebChat native methods)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt;: &lt;code&gt;skills.install&lt;/code&gt;, &lt;code&gt;skills.update&lt;/code&gt;, &lt;code&gt;skills.bins&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS&lt;/strong&gt;: &lt;code&gt;tts.convert&lt;/code&gt;, &lt;code&gt;tts.providers&lt;/code&gt;, &lt;code&gt;tts.setProvider&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins inject additional method handlers via &lt;code&gt;pluginRegistry.gatewayHandlers&lt;/code&gt;; channel plugins contribute method names via &lt;code&gt;plugin.gatewayMethods&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow Consumer Protection
&lt;/h3&gt;

&lt;p&gt;The broadcast function &lt;code&gt;broadcastInternal()&lt;/code&gt; (&lt;code&gt;src/gateway/server-broadcast.ts&lt;/code&gt;) checks each client's &lt;code&gt;socket.bufferedAmount&lt;/code&gt; before sending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFERED_BYTES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 50 MB&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;dropIfSlow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// Low-priority events like tick / heartbeat are simply dropped&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1008&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slow consumer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Critical events: forcefully disconnect slow client&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents a single slow-consuming client from causing unbounded memory growth in the Gateway process. &lt;code&gt;dropIfSlow: true&lt;/code&gt; is used for keepalive events like &lt;code&gt;tick&lt;/code&gt; and &lt;code&gt;heartbeat&lt;/code&gt; where loss is acceptable, while business-critical events like &lt;code&gt;agent&lt;/code&gt; and &lt;code&gt;health&lt;/code&gt; force-disconnect slow clients.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Configuration Hot-Reload
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four Reload Modes
&lt;/h3&gt;

&lt;p&gt;The hot-reload system is defined in &lt;code&gt;src/gateway/config-reload.ts&lt;/code&gt; and &lt;code&gt;src/gateway/config-reload-plan.ts&lt;/code&gt;, configured via &lt;code&gt;gateway.reload.mode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Default configuration (&lt;code&gt;src/gateway/config-reload.ts:16-19&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/config-reload.ts:16-19&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_RELOAD_SETTINGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GatewayReloadSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hybrid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;debounceMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;off&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Completely disabled; file changes trigger no action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any change triggers a full gateway restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only performs hot-reload; changes requiring restart are &lt;strong&gt;ignored&lt;/strong&gt; (prints a warning log)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hybrid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Default&lt;/strong&gt;: attempts hot-reload first; triggers a restart if the change involves paths that require it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Chokidar File Watching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chokidar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watchPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ignoreInitial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;awaitWriteFinish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stabilityThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pollInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// File stability threshold 200ms&lt;/span&gt;
  &lt;span class="na"&gt;usePolling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITEST&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stabilityThreshold: 200&lt;/code&gt; prevents multiple reloads when editors write files in multiple passes (e.g., VS Code's safe write mechanism). &lt;code&gt;debounceMs&lt;/code&gt; (default 300ms) is read from configuration by &lt;code&gt;resolveGatewayReloadSettings()&lt;/code&gt; and adds an additional delay after the chokidar event fires before performing the actual snapshot read.&lt;/p&gt;

&lt;h3&gt;
  
  
  diffConfigPaths: Recursive Deep Diff
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;diffConfigPaths(prev, next, prefix)&lt;/code&gt; (&lt;code&gt;src/gateway/config-reload.ts:23-52&lt;/code&gt;) is a pure function that recursively compares two configuration objects and returns a list of changed dot-path strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;diffConfigPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;old&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ["gateway.auth.token"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For array types, it uses &lt;code&gt;isDeepStrictEqual&lt;/code&gt; for whole-array comparison rather than per-element expansion, avoiding false-positive index paths (such as &lt;code&gt;memory.qmd.paths.0&lt;/code&gt;) for fields like &lt;code&gt;memory.qmd.paths&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GatewayReloadPlan: Per-Subsystem Restart Flags
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;buildGatewayReloadPlan(changedPaths)&lt;/code&gt; (&lt;code&gt;src/gateway/config-reload-plan.ts:137&lt;/code&gt;) maps the diff results to a &lt;code&gt;GatewayReloadPlan&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/config-reload-plan.ts:6-19&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GatewayReloadPlan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;changedPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;restartGateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// true → requires full restart&lt;/span&gt;
  &lt;span class="nl"&gt;restartReasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;       &lt;span class="c1"&gt;// Paths that triggered restart&lt;/span&gt;
  &lt;span class="nl"&gt;hotReasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;           &lt;span class="c1"&gt;// Paths handled via hot-reload&lt;/span&gt;
  &lt;span class="nl"&gt;reloadHooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Reload Webhook configuration&lt;/span&gt;
  &lt;span class="nl"&gt;restartGmailWatcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Restart Gmail watcher&lt;/span&gt;
  &lt;span class="nl"&gt;restartBrowserControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Restart Browser Control service&lt;/span&gt;
  &lt;span class="nl"&gt;restartCron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Rebuild CronService&lt;/span&gt;
  &lt;span class="nl"&gt;restartHeartbeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Rebuild HeartbeatRunner&lt;/span&gt;
  &lt;span class="nl"&gt;restartHealthMonitor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Rebuild ChannelHealthMonitor&lt;/span&gt;
  &lt;span class="nl"&gt;restartChannels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChannelKind&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Set of channels that need restart&lt;/span&gt;
  &lt;span class="nl"&gt;noopPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;            &lt;span class="c1"&gt;// No-op paths (known safe to ignore)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BASE_RELOAD_RULES: Prefix-Priority Matching Rules
&lt;/h3&gt;

&lt;p&gt;The rule table (&lt;code&gt;src/gateway/config-reload-plan.ts:36-70&lt;/code&gt;) uses a "prefix-priority, first-match" approach. &lt;code&gt;matchRule(path)&lt;/code&gt; iterates through the rule list and returns the first rule where &lt;code&gt;path === rule.prefix || path.startsWith(rule.prefix + ".")&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/config-reload-plan.ts:36-70 — Key rules excerpt&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_RELOAD_RULES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReloadRule&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gateway.remote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gateway.reload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gateway.channelHealthCheckMinutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restart-health-monitor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hooks.gmail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restart-gmail-watcher&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hooks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reload-hooks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agents.defaults.heartbeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restart-heartbeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;models&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restart-heartbeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restart-cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restart-browser-control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complete prefix matching map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gateway.remote          → none    (remote config path, noop)
gateway.reload          → none    (reload's own config, prevents reload loop)
gateway.channelHealthTiming → hot + restart-health-monitor
hooks.gmail             → hot + restart-gmail-watcher
hooks                   → hot + reload-hooks
agents.defaults.heartbeat → hot + restart-heartbeat
models                  → hot + restart-heartbeat
cron                    → hot + restart-cron
browser                 → hot + restart-browser-control
plugins                 → restart  (plugin changes require restart)
gateway                 → restart  (other gateway config requires restart)
discovery               → restart
canvasHost              → restart
meta / identity / agents / tools / routing / ... → none (noop)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paths that do not match any rule default to &lt;code&gt;restart&lt;/code&gt; (conservative principle).&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid Mode Decision Tree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File change detected (chokidar)
│
├─ debounce 300ms
│
└─ runReload()
    ├─ readConfigFileSnapshot()
    ├─ handleMissingSnapshot()   → If missing, retry 2 times (150ms interval); if still missing, warn and skip
    ├─ handleInvalidSnapshot()   → If Zod validation fails, warn and skip
    └─ applySnapshot(nextConfig)
        ├─ diffConfigPaths(currentConfig, nextConfig)
        ├─ changedPaths.length == 0  → no-op
        ├─ mode == "off"             → log + no-op
        ├─ mode == "restart"         → queueRestart(plan, nextConfig)
        ├─ plan.restartGateway == true
        │   ├─ mode == "hot"         → warn log + ignore (no restart)
        │   └─ mode == "hybrid"      → queueRestart(plan, nextConfig)
        └─ plan.restartGateway == false
            └─ onHotReload(plan, nextConfig)  → applyHotReload() (atomically switch each subsystem)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  openclaw.json Hot-Reload Configuration Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hybrid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"debounceMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable auto-reload (suitable for production with manual control):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Force all changes to go through a full restart (most conservative mode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restart"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Health Monitoring and Presence
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ChannelHealthMonitor Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;startChannelHealthMonitor()&lt;/code&gt; (&lt;code&gt;src/gateway/channel-health-monitor.ts:77&lt;/code&gt;) implements periodic health checks for all configured channel accounts, automatically restarting unhealthy channel connections.&lt;/p&gt;

&lt;p&gt;Default timing parameters (&lt;code&gt;src/gateway/channel-health-monitor.ts:12-25&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/channel-health-monitor.ts:12-25&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_CHECK_INTERVAL_MS&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;//  5 minutes (check interval)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_MONITOR_STARTUP_GRACE_MS&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;//  1 minute (monitor startup grace period)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_COOLDOWN_CYCLES&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;//  → cooldown = 2 × 5min = 10 minutes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_MAX_RESTARTS_PER_HOUR&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;//  Max 10 restarts per hour&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ONE_HOUR_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_STALE_EVENT_THRESHOLD_MS&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 30 minutes (no events → stale socket)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_CHANNEL_CONNECT_GRACE_MS&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;//  2 minutes (channel connect grace period)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where this function is called in the Gateway (&lt;code&gt;src/gateway/server.impl.ts:694-699&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/gateway/server.impl.ts:694-699&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;channelHealthMonitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;healthCheckDisabled&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;startChannelHealthMonitor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;channelManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;checkIntervalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;healthCheckMinutes&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "stale socket" scenario targets platforms like Slack where WebSocket connections can appear alive (health check passes) while the platform has silently stopped pushing events. By tracking the &lt;code&gt;lastEventAt&lt;/code&gt; timestamp, a restart is triggered if no events are received for 30 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  evaluateChannelHealth Decision Tree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runCheck() (every 5 minutes)
│
├─ now - startedAt &amp;lt; monitorStartupGraceMs (1min)  → skip all checks
│
└─ for each channelId / accountId in snapshot:
    ├─ isManuallyStopped?  → skip
    ├─ evaluateChannelHealth(status, policy)
    │   ├─ healthy == true  → skip
    │   └─ healthy == false
    │       ├─ Check cooldown: now - lastRestartAt &amp;lt;= cooldownMs (10min)  → skip
    │       ├─ pruneOldRestarts (sliding window, 1 hour)
    │       ├─ restartsThisHour &amp;gt;= maxRestartsPerHour (10)  → warn + skip
    │       └─ Execute restart:
    │           ├─ stopChannel(channelId, accountId)
    │           ├─ resetRestartAttempts()
    │           ├─ startChannel(channelId, accountId)
    │           └─ Update RestartRecord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart records (&lt;code&gt;RestartRecord&lt;/code&gt;) are stored in a &lt;code&gt;Map&amp;lt;string, RestartRecord&amp;gt;&lt;/code&gt; keyed by &lt;code&gt;"channelId:accountId"&lt;/code&gt;, supporting independent counters for each account in multi-account scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adjusting Timing via Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;resolveHealthTimingFromConfig()&lt;/code&gt; converts user-configured "minutes" into internal milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channelHealthCheckMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channelHealthTiming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"startupGraceMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"connectGraceMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"staleEventThresholdMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;channelHealthCheckMinutes: 0&lt;/code&gt; &lt;strong&gt;completely disables&lt;/strong&gt; health monitoring (&lt;code&gt;healthCheckDisabled = true&lt;/code&gt;, no monitor instance is created).&lt;/li&gt;
&lt;li&gt;The three fields in &lt;code&gt;channelHealthTiming&lt;/code&gt; are independently configurable; unset fields use their default values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This configuration change triggers the &lt;code&gt;restart-health-monitor&lt;/code&gt; action, categorized as &lt;code&gt;hot&lt;/code&gt;, meaning the Gateway does not need a full restart for it to take effect (when used with &lt;code&gt;hybrid&lt;/code&gt; or &lt;code&gt;hot&lt;/code&gt; mode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hybrid"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channelHealthCheckMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Presence System
&lt;/h3&gt;

&lt;p&gt;Presence works in conjunction with health monitoring. When clients connect or disconnect, &lt;code&gt;upsertPresence()&lt;/code&gt; updates the online status, &lt;code&gt;incrementPresenceVersion()&lt;/code&gt; increments the version number, and &lt;code&gt;broadcastPresenceSnapshot()&lt;/code&gt; broadcasts the latest presence snapshot to all connected clients. The presence version number is sent alongside the &lt;code&gt;event: tick&lt;/code&gt; frame (every 30 seconds) and all broadcast &lt;code&gt;stateVersion&lt;/code&gt; fields, enabling clients to detect whether they need to request a refresh.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Global Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                    OpenClaw Gateway (single process, single port 18789) │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                   HTTP / TLS Layer (Node http/https)          │   │
│  │                                                             │   │
│  │  incoming request                                           │   │
│  │       │                                                     │   │
│  │  Upgrade: websocket? ──YES──→ attachGatewayUpgradeHandler   │   │
│  │       │                              │                      │   │
│  │       NO                             ↓                      │   │
│  │       ↓                       WebSocketServer               │   │
│  │  handleRequest()              (maxPayload=25MB)             │   │
│  │  Stage Pipeline:              attachGatewayWsConnectionHandler│ │
│  │  [hooks]                      ├─ connect.challenge          │   │
│  │  [tools-invoke]               ├─ handshake (10s timeout)    │   │
│  │  [slack]                      ├─ hello-ok (methods+events)  │   │
│  │  [openresponses]              └─ req/res/event frames       │   │
│  │  [openai]                                                   │   │
│  │  [canvas-auth]                                              │   │
│  │  [a2ui]                                                     │   │
│  │  [canvas-http]                                              │   │
│  │  [plugin-auth]                                              │   │
│  │  [plugin-http]                                              │   │
│  │  [control-ui-avatar]                                        │   │
│  │  [control-ui-http]                                          │   │
│  │  [gateway-probes]                                           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
│  │ NodeRegistry │  │ CronService  │  │   ChannelManager         │  │
│  │ (Pi/mobile/  │  │ (scheduled   │  │ Slack/Telegram/Discord/  │  │
│  │  desktop     │  │  tasks)      │  │ Signal/iMessage/...      │  │
│  │  nodes)      │  │              │  │                          │  │
│  └──────────────┘  └──────────────┘  └──────────┬───────────────┘  │
│                                                  │                  │
│  ┌────────────────────────────────┐   ┌──────────▼───────────────┐  │
│  │   Maintenance Timers           │   │  ChannelHealthMonitor    │  │
│  │  tick:   30s  keepalive        │   │  check: 5min             │  │
│  │  health: 60s  snapshot refresh │   │  stale: 30min            │  │
│  │  dedupe: 60s  cache cleanup    │   │  max restarts: 10/hr     │  │
│  └────────────────────────────────┘   └──────────────────────────┘  │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │               Config Reloader (chokidar)                     │   │
│  │  openclaw.json → diffConfigPaths → buildGatewayReloadPlan   │   │
│  │  stabilityThreshold: 200ms  debounce: 300ms                 │   │
│  │  mode: off / restart / hot / hybrid(default)                │   │
│  │                                                              │   │
│  │  hot reload:  hooks / heartbeat / cron / health-monitor      │   │
│  │               / browser-control / per-channel restart        │   │
│  │  restart:     gateway / plugins / discovery / canvasHost     │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌────────────────────┐  ┌────────────────────┐                    │
│  │   Secrets Runtime  │  │  HeartbeatRunner   │                    │
│  │  activateSnapshot  │  │  (agent heartbeat  │                    │
│  │  last-known-good   │  │   loop)            │                    │
│  └────────────────────┘  └────────────────────┘                    │
│                                                                     │
│  Sidecars: BrowserControl · GmailWatcher · Tailscale · PluginSvcs  │
└─────────────────────────────────────────────────────────────────────┘
         ↑ WS clients: macOS app / iOS / Android / Web UI / Pi nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Core Source File Quick Reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Source File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gateway main entry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server.impl.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP routing Stage Pipeline&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-http.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket connection handshake&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server/ws-connection.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WS frame schema&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/protocol/schema/frames.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WS broadcast + Slow Consumer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-broadcast.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server constants (MAX_PAYLOAD, etc.)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-constants.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime state initialization&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-runtime-state.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance timers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-maintenance.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration hot-reload&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/config-reload.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reload plan construction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/config-reload-plan.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel health monitoring&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/channel-health-monitor.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway method list&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-methods-list.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup sidecars&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/gateway/server-startup.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;We traced the Gateway's single-process, single-port architecture and how Node.js's native &lt;code&gt;upgrade&lt;/code&gt; event multiplexes HTTP and WebSocket traffic without a reverse proxy.&lt;/li&gt;
&lt;li&gt;The 48-step, 9-phase startup sequence was examined end to end -- from config migration and secrets resolution through plugin loading, HTTP/WS server creation, sidecar lifecycle management, and the config reloader.&lt;/li&gt;
&lt;li&gt;HTTP routing uses a stage pipeline with first-match-wins semantics across 13 ordered stages, where disabled features contribute zero overhead because their stages are never added to the array.&lt;/li&gt;
&lt;li&gt;Configuration hot-reload was broken down across its four modes (off/restart/hot/hybrid), the recursive deep-diff engine, and the per-subsystem reload rules that determine which changes can be applied without a full restart.&lt;/li&gt;
&lt;li&gt;Health monitoring and slow-consumer protection ensure operational resilience: channel health checks auto-restart unhealthy connections with cooldown and hourly caps, while broadcast backpressure either drops low-priority events or force-disconnects laggy WebSocket clients.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article is based on source code analysis of OpenClaw 2026.3.2. All data comes from actual code, with no subjective inference.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;--&lt;br&gt;
*&lt;a href="https://agentinternals.substack.com" rel="noopener noreferrer"&gt;Originally published on Agent Internals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>networking</category>
      <category>node</category>
    </item>
  </channel>
</rss>
