import path from 'path'; import { STARTUP_PRECONDITION_EXIT_CODE } from '../src/startup-preconditions.js'; import type { ServiceDef } from './service-defs.js'; export function buildRuntimePathEnv(nodePath: string, homeDir: string): string { return `${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin`; } function buildLaunchdEnvironmentEntries( nodePath: string, homeDir: string, extraEnv?: Record, ): string[] { const envEntries = [ ` PATH`, ` ${buildRuntimePathEnv(nodePath, homeDir)}`, ` HOME`, ` ${homeDir}`, ]; if (extraEnv) { for (const [k, v] of Object.entries(extraEnv)) { envEntries.push(` ${k}`); envEntries.push(` ${v}`); } } return envEntries; } function buildSystemdEnvironmentLines( nodePath: string, homeDir: string, extraEnv?: Record, ): string[] { const envLines = [ `Environment=HOME=${homeDir}`, `Environment=PATH=${buildRuntimePathEnv(nodePath, homeDir)}`, ]; if (extraEnv) { for (const [k, v] of Object.entries(extraEnv)) { envLines.push(`Environment=${k}=${v}`); } } return envLines; } export function buildLaunchdPlist( def: ServiceDef, projectRoot: string, nodePath: string, homeDir: string, ): string { const envEntries = buildLaunchdEnvironmentEntries( nodePath, homeDir, def.extraEnv, ); return ` Label ${def.launchdLabel} ProgramArguments ${nodePath} ${projectRoot}/dist/index.js WorkingDirectory ${projectRoot} RunAtLoad KeepAlive EnvironmentVariables ${envEntries.join('\n')} StandardOutPath ${projectRoot}/logs/${def.logName}.log StandardErrorPath ${projectRoot}/logs/${def.logName}.error.log `; } export function buildSystemdUnit( def: ServiceDef, projectRoot: string, nodePath: string, homeDir: string, runningAsRoot: boolean, ): string { const envLines = buildSystemdEnvironmentLines( nodePath, homeDir, def.extraEnv, ); const envFileLine = def.environmentFile ? `EnvironmentFile=${def.environmentFile}\n` : ''; return `[Unit] Description=${def.description} After=network.target [Service] ${envFileLine}Type=simple ExecStart=${nodePath} ${projectRoot}/dist/index.js WorkingDirectory=${projectRoot} Restart=always RestartSec=5 RestartPreventExitStatus=${STARTUP_PRECONDITION_EXIT_CODE} # Per-turn agent runners do heavy file I/O (git clone, builds, video/ffmpeg # scratch) inside this service cgroup. The kernel keeps those reads as # reclaimable page cache, which makes systemd's MemoryCurrent climb to several # GB even though real (anon) usage stays a few hundred MB. MemoryHigh applies # gentle reclaim pressure so that cache is dropped before the number balloons, # while MemoryMax stays unset (infinity) so a legitimate burst is never # OOM-killed — it is throttled at worst. MemoryAccounting=yes MemoryHigh=3G ${envLines.join('\n')} StandardOutput=append:${projectRoot}/logs/${def.logName}.log StandardError=append:${projectRoot}/logs/${def.logName}.error.log [Install] WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`; } export function buildStackRestartSystemdUnit( projectRoot: string, nodePath: string, homeDir: string, ): string { const envLines = buildSystemdEnvironmentLines(nodePath, homeDir); return `[Unit] Description=EJClaw Stack Restart Orchestrator After=network.target [Service] Type=oneshot WorkingDirectory=${projectRoot} ${envLines.join('\n')} ExecStart=${nodePath} ${projectRoot}/setup/restart-stack.ts --direct `; }