Daemon: system service
Responsibilities
resolveGatewayService (resolveGatewayService:L370-L375) is OpenClaw's unified entry for installing the gateway as a system-level daemon: LaunchAgent on macOS (managed by launchd), systemd user unit on Linux (managed by systemd --user), Scheduled Task on Windows (registered via schtasks with a logon trigger). The three platforms' commands are wrapped into the same GatewayService (GatewayService:L75-L87) interface: stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime; the CLI layer doesn't care about platform differences.
It doesn't handle "running the gateway" — the gateway itself is still node openclaw.mjs gateway. The daemon layer does write that command into the system service manifest (plist/unit/XML), then let the system service manager pull it up, monitor, and auto-restart per rules. So the daemon module has a lot of code on plist/unit/XML template rendering and launchctl/systemctl/schtasks CLI invocation; the actual gateway process lifecycle is controlled by the system service manager.
Design motivation
Why not just nohup openclaw gateway &?
- Boot auto-start + crash restart:
launchd'sKeepAlive=true, systemd'sRestart=always, schtasks'sLogonTriggerare all system-level mechanisms that auto-start on machine reboot or unexpected OpenClaw exit.nohuponly "ignores SIGHUP"; on machine reboot it's dead. - Unified config + profile isolation:
OPENCLAW_PROFILElets one machine run multiple gateways (e.g. dev/prod separate).resolveGatewayLaunchAgentLabel(resolveGatewayLaunchAgentLabel:L33-L39) bakes the profile into the service label —ai.openclaw.gateway(default) orai.openclaw.<profile>; systemd and schtasks get corresponding suffixes. Each profile gets an independent plist/unit/XML, no interference. - future config guard:
withFutureConfigGuard(withFutureConfigGuard:L336-L362) callsassertFutureConfigActionAllowedbefore each write operation (stage/install/uninstall/stop/restart). If it detects the current OpenClaw version is newer than what's in the service config (or vice versa, config written by a future version), it refuses to modify — a safety net against "downgrade start corrupting new-format service files". - repair detection:
collectGatewayServiceStartRepairIssues(collectGatewayServiceStartRepairIssues:L138-L172) checks the loaded service'sOPENCLAW_SERVICE_VERSION, whether the program path points at a temp directory (isTemporaryProgramPath), and whether the program file still exists (isMissingProgramPath) beforerestart. Version drift or path invalidation requires reinstall, not a fake "restart succeeded".
Key files
GatewayService type:L75-L87— 8-method unified interface.GATEWAY_SERVICE_REGISTRY:L294-L334—darwin/linux/win32three adapter sets, each corresponding to a platform implementation module.withFutureConfigGuard:L336-L362— wraps stage/install/uninstall/stop/restart with a pre-writeassertFutureConfigActionAllowedcheck.resolveGatewayService:L370-L375— picks adapter byprocess.platform; unknown platforms returncreateUnsupportedGatewayService(all methods throwunsupported).isTemporaryProgramPath:L115-L136—/tmp//var/tmp/os.tmpdir()path detection; services pointing at temp dirs are flagged for repair.collectGatewayServiceStartRepairIssues:L138-L172—OPENCLAW_SERVICE_VERSIONdrift + temp path + missing file; three classes of start repair issues.service label constants:L5-L17—GATEWAY_LAUNCH_AGENT_LABEL="ai.openclaw.gateway",GATEWAY_SYSTEMD_SERVICE_NAME="openclaw-gateway",GATEWAY_WINDOWS_TASK_NAME="OpenClaw Gateway", legacyclawdbot-gateway.profile-aware names:L33-L60—resolveGatewayLaunchAgentLabel/resolveGatewaySystemdServiceName/resolveGatewayWindowsTaskName; profile suffix rules.installLaunchAgent:L1013-L1030—writeLaunchAgentPlist+activateLaunchAgent; macOS load.launchd constants:L5-L13—LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS=10,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=20,LAUNCH_AGENT_UMASK_DECIMAL=0o077(owner-only files).buildLaunchAgentPlist:L267-L295— plist rendering:RunAtLoad=true/KeepAlive=true/ExitTimeOut=20/ThrottleInterval=10/Umask=0o077/StandardOutPath/StandardErrorPath.installSystemdService:L1107-L1131—writeSystemdUnit+activateSystemdService.buildSystemdUnit:L52-L101—[Unit]/[Service]/[Install]three sections;Restart=always/RestartSec=5/TimeoutStopSec=30/OOMPolicy=continue/KillMode=control-group.readSystemdUserLingerStatus:L28-L50— headless deployments needloginctl enable-linger; this reads the status.buildScheduledTaskXml:L139-L192— Windows Task Scheduler XML;LogonTrigger/LeastPrivilege/DisallowStartIfOnBatteries=false.installScheduledTask:L1313-L1325—writeScheduledTaskScript+activateScheduledTask.writeTaskXmlTempFile:L194-L203—schtasks /XMLrequires UTF-16 LE BOM; Nodeutf16le+ hand-writtenFFFEBOM.GATEWAY_DIST_ENTRYPOINT_BASENAMES:L5-L22—index.js/index.mjs/entry.js/entry.mjs; picks an available entry from the dist directory.
Data flow
resolveGatewayService (resolveGatewayService:L370) picks an adapter from the platform registry:
const GATEWAY_SERVICE_REGISTRY: Record<SupportedGatewayServicePlatform, GatewayService> = {
darwin: {
label: "LaunchAgent",
loadedText: "loaded",
notLoadedText: "not loaded",
stage: ignoreServiceWriteResult(stageLaunchAgent),
install: ignoreServiceWriteResult(installLaunchAgent),
uninstall: uninstallLaunchAgent,
stop: stopLaunchAgent,
restart: restartLaunchAgent,
isLoaded: isLaunchAgentLoaded,
readCommand: readLaunchAgentProgramArguments,
readRuntime: readLaunchAgentRuntime,
},
linux: { label: "systemd user", /* ...systemd impl */ },
win32: { label: "Scheduled Task", /* ...schtasks impl */ },
};
export function resolveGatewayService(): GatewayService {
if (isSupportedGatewayServicePlatform(process.platform)) {
return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]);
}
return createUnsupportedGatewayService();
}withFutureConfigGuard wraps all write methods with assertFutureConfigActionAllowed — a unified "check before write" interception, preventing an older OpenClaw from corrupting service files written by a newer version.
The launchd plist template (buildLaunchAgentPlist:L267) is the most complex:
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${plistEscape(label)}</string>
${commentXml}
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ExitTimeOut</key><integer>${LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS}</integer>
<key>ProcessType</key><string>${LAUNCH_AGENT_PROCESS_TYPE}</string>
<key>ThrottleInterval</key><integer>${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}</integer>
<key>Umask</key><integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>
<key>ProgramArguments</key>
<array>${argsXml}
</array>
${workingDirXml}
<key>StandardInPath</key><string>/dev/null</string>
<key>StandardOutPath</key><string>${plistEscape(stdoutPath)}</string>
<key>StandardErrorPath</key><string>${plistEscape(stderrPath)}</string>${envXml}
</dict>
</plist>
`;Four key fields: RunAtLoad=true (start immediately on load), KeepAlive=true (restart on any exit), ExitTimeOut=20 (wait 20s after SIGTERM before SIGKILL), ThrottleInterval=10 (launchd's built-in crash-loop protection; at least 10s between launches). Umask=0o077 makes all files gateway creates default to owner-only — preventing group/other from reading secrets. envXml embeds OPENCLAW_STATE_DIR/OPENCLAW_PROFILE etc. into the plist, so no extra env-file is needed.
The systemd unit (buildSystemdUnit:L68) uses a standard three-section structure:
return [
"[Unit]",
descriptionLine,
"After=network-online.target",
"Wants=network-online.target",
"StartLimitBurst=5",
"StartLimitIntervalSec=60",
"",
"[Service]",
`ExecStart=${execStart}`,
"Restart=always",
"RestartSec=5",
"RestartPreventExitStatus=78", // EX_CONFIG: don't restart on config error, avoiding an infinite loop
"TimeoutStopSec=30",
"TimeoutStartSec=30",
"SuccessExitStatus=0 143", // 143 = SIGTERM termination counts as normal exit
"OOMPolicy=continue", // child OOM-killed doesn't take down main service
"KillMode=control-group", // on restart KILL all children, avoiding orphan ACP workers
workingDirLine,
...environmentFileLines,
...envLines,
"",
"[Install]",
"WantedBy=default.target",
"",
].filter((line) => line !== null).join("\n");OOMPolicy=continue and KillMode=control-group are two non-default values — the former ensures a child OOM doesn't take down the whole gateway; the latter ensures no ACP/agent worker orphans on gateway restart. StartLimitBurst=5/StartLimitIntervalSec=60 is systemd's built-in crash-loop protection — 5 crashes within 60s stops restart (requires manual intervention).
Windows schtasks XML (buildScheduledTaskXml:L152) uses LogonTrigger (trigger on user logon), LeastPrivilege (no admin), DisallowStartIfOnBatteries=false (run on laptop battery too). Hidden pitfall: schtasks /XML requires UTF-16 LE BOM (writeTaskXmlTempFile:L194); Node's utf16le encoding doesn't add BOM automatically, so the code manually prepends Buffer.from([0xff, 0xfe]) + body.
Boundaries and failures
- unsupported platform degrades:
createUnsupportedGatewayService(createUnsupportedGatewayService:L275-L292) — all write methods throwcreateUnsupportedGatewayServiceError(),readCommandreturns null,readRuntimereturns{ status: "unknown", detail: ... }. Systems like FreeBSD/OpenBSD whoseprocess.platformisn't in the table get this degraded service. - temp path detection:
TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"](TEMP_PROGRAM_ROOTS:L115). If the loaded service's program path points at these directories,collectGatewayServiceStartRepairIssuesflags an issue — temp dirs can be cleaned by the system, and the service suddenly can't find the executable. This state requires reinstall, not a simple restart. - version drift protection:
serviceVersion !== VERSIONis also an issue (OPENCLAW_SERVICE_VERSION check:L146-L150).OPENCLAW_SERVICE_VERSIONis written into the plist/unit/XML env vars at install and read out at runtime to compare against the current OpenClaw version — mismatch means the service was installed by an older version that may reference deleted binary paths or old working-dir layouts. - launchd doesn't kickstart -k:
installLaunchAgent(avoid kickstart -k:L1018-L1020) comments explicitly thatbootstrapalready RunAtLoads; don't alsokickstart -k— on slow macOS VMs,kickstart -kSIGTERMs the just-started gateway, pushing the real listener startup past the setup health-check deadline. - launchd spawn throttle:
LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10(LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:L8) is launchd's default; explicitly written into the plist to make crash loops at least 10s apart, not every second.LAUNCH_AGENT_UMASK_DECIMAL = 0o077is explicitly rendered as decimal63— launchd's plist integer field doesn't accept0o077notation. - systemd ExitStatus 78:
RestartPreventExitStatus=78(RestartPreventExitStatus:L80) is EX_CONFIG — when the gateway exits with code 78 on config error, systemd sees it and doesn't restart. This avoids the infinite loop of "config file broken, systemd keeps restarting and crashing". - systemd linger:
readSystemdUserLingerStatus(readSystemdUserLingerStatus:L28) readsloginctl show-user <user> -p Linger. In headless deployments (no active SSH session), the systemd user instance doesn't start, so the gateway can't run — needsloginctl enable-linger <user>. The daemon module reads status but doesn't force-modify, leaving the decision to the admin. - schtasks UTF-16 BOM:
writeTaskXmlTempFile(writeTaskXmlTempFile:L194) must writeFFFEBOM, otherwise Task Scheduler MMC refuses to import under some locales. This is a Windows toolchain compat detail, explicitly commented in code. - schtasks upgrade Change + XML:
updateExistingScheduledTask(updateExistingScheduledTask:L1042) first/Changeto modify/TR(task command), then/Create /F /XMLto overwrite the whole XML — this ensures old-version-installed tasks can inherit new fields like<DisallowStartIfOnBatteries>false</...>on upgrade (#59299). Both steps best-effort;/Changefailure retains old settings, doesn't lose the task.
Summary
The daemon module is OpenClaw's thin wrapper around three system service managers (launchd/systemd/schtasks): it doesn't run the gateway, only writes plist/unit/XML + calls system CLIs to load/unload/restart. resolveGatewayService picks an adapter by process.platform; withFutureConfigGuard protects write operations from version mismatch corruption; collectGatewayServiceStartRepairIssues detects version drift / path invalidation / temp dirs before restart. The actual gateway startup logic is in Gateway core; containerized deployment (no system service manager) is in Docker and Fly deployment; profile switching and service env vars are in openclaw.json.