1"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([["3424"],{9602(e,s,n){n.r(s),n.d(s,{metadata:()=>t,default:()=>h,frontMatter:()=>o,contentTitle:()=>i,toc:()=>l,assets:()=>d});var t=JSON.parse('{"id":"using/how-to/run-as-daemon","title":"Run Ethos as a daemon","description":"Run ethos gateway start (or any long-running ethos command) under systemd, launchd, Windows Task Scheduler, or pm2 as a persistent process.","source":"@site/content/using/how-to/run-as-daemon.md","sourceDirName":"using/how-to","slug":"/using/how-to/run-as-daemon","permalink":"/docs/using/how-to/run-as-daemon","draft":false,"unlisted":false,"editUrl":"https://github.com/ethosagent/ethos/tree/main/docs/content/using/how-to/run-as-daemon.md","tags":[],"version":"current","frontMatter":{"title":"Run Ethos as a daemon","description":"Run ethos gateway start (or any long-running ethos command) under systemd, launchd, Windows Task Scheduler, or pm2 as a persistent process.","kind":"how-to","audience":"user","slug":"run-as-daemon","time":"10 min","updated":"2026-09-25T00:00:00.000Z"},"sidebar":"docsSidebar","previous":{"title":"Run Ethos in Docker","permalink":"/docs/using/how-to/run-in-docker"},"next":{"title":"Use zero mode","permalink":"/docs/using/how-to/use-zero-mode"}}'),r=n(8265),a=n(7214);let o={title:"Run Ethos as a daemon",description:"Run ethos gateway start (or any long-running ethos command) under systemd, launchd, Windows Task Scheduler, or pm2 as a persistent process.",kind:"how-to",audience:"user",slug:"run-as-daemon",time:"10 min",updated:new Date("2026-09-25T00:00:00.000Z")},i,d={},l=[{value:"Task",id:"task",level:2},{value:"Result",id:"result",level:2},{value:"Prereqs",id:"prereqs",level:2},{value:"What can run as a daemon",id:"what-can-run-as-a-daemon",level:2},{value:"Steps",id:"steps",level:2},{value:"1. Foreground-test first",id:"1-foreground-test-first",level:3},{value:"2A. macOS \u2014 launchd",id:"2a-macos--launchd",level:3},{value:"2B. Linux \u2014 systemd user unit",id:"2b-linux--systemd-user-unit",level:3},{value:"2C. Cross-platform \u2014 pm2",id:"2c-cross-platform--pm2",level:3},{value:"2D. Windows \u2014 Task Scheduler",id:"2d-windows--task-scheduler",level:3},{value:"3. Update the daemon after <code>ethos upgrade</code>",id:"3-update-the-daemon-after-ethos-upgrade",level:3},{value:"Verify",id:"verify",level:2},{value:"Operator concerns",id:"operator-concerns",level:2},{value:"Linger (headless Linux)",id:"linger-headless-linux",level:3}
1,{value:"Detached child processes",id:"detached-child-processes",level:3},{value:"Troubleshoot",id:"troubleshoot",level:2}];function c(e){let s={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Looking for the full production setup?"})," If you want ",(0,r.jsx)(s.strong,{children:"both"})," the gateway (Telegram + Slack + Discord + Email) ",(0,r.jsx)(s.strong,{children:"and"})," the web dashboard up under one supervisor, with reboot survival on a mini-PC / VPS / home server, jump to ",(0,r.jsx)(s.a,{href:"/docs/using/how-to/deploy-in-production",children:"Deploy in production"})," \u2014 it uses ",(0,r.jsx)(s.code,{children:"ethos run-all"})," and PM2 and is the shorter path. This page is the building-block reference for daemonising a ",(0,r.jsx)(s.strong,{children:"single"})," ",(0,r.jsx)(s.code,{children:"ethos"})," command (the gateway by itself, or cron, or serve)."]}),"\n"]}),"\n",(0,r.jsx)(s.h2,{id:"task",children:"Task"}),"\n",(0,r.jsxs)(s.p,{children:["Run ",(0,r.jsx)(s.code,{children:"ethos gateway start"})," (or another long-running ",(0,r.jsx)(s.code,{children:"ethos"})," subcommand) as a persistent background process under systemd, launchd, Windows Task Scheduler, or pm2, surviving logout and restarting on crash. The ",(0,r.jsx)(s.a,{href:"/docs/getting-started/glossary#gateway",children:"gateway"})," is the long-running process that routes platform messages into the agent loop."]}),"\n",(0,r.jsx)(s.h2,{id:"result",children:"Result"}),"\n",(0,r.jsx)(s.p,{children:"The gateway answers your bot on Telegram, Slack, Discord, WhatsApp, or email without a terminal open, restarts on failure, and starts again on boot."}),"\n",(0,r.jsx)(s.h2,{id:"prereqs",children:"Prereqs"}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.code,{children:"ethos"})," installed; ",(0,r.jsx)(s.code,{children:"ethos --version"})," returns a version string."]}),"\n",(0,r.jsxs)(s.li,{children:["A provider configured via ",(0,r.jsx)(s.code,{children:"ethos setup"})," (",(0,r.jsx)(s.a,{href:"/docs/using/how-to/configure-providers",children:"Configure an LLM provider"}),")."]}),"\n",(0,r.jsxs)(s.li,{children:["For gateway use, at least one platform token in ",(0,r.jsx)(s.code,{children:"~/.ethos/config.yaml"})," (",(0,r.jsx)(s.code,{children:"telegramToken"}),", ",(0,r.jsx)(s.code,{children:"slackBotToken"}),", etc.)."]}),"\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.code,{children:"which ethos"})," returns an absolute path. If you installed via ",(0,r.jsx)(s.code,{children:"nvm"}),", that path is under ",(0,r.jsx)(s.code,{children:"~/.nvm/versions/node/..."})," \u2014 service managers cannot resolve a bare ",(0,r.jsx)(s.code,{children:"ethos"})," without your shell."]}),"\n"]}),"\n",(0,r.jsx)(s.h2,{id:"what-can-run-as-a-daemon",children:"What can run as a daemon"}),"\n",(0,r.jsxs)(s.p,{children:["Four ",(0,r.jsx)(s.code,{children:"ethos"})," subcommands are long-running. Everything else is one-shot or REPL."]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Command"}),(0,r.jsx)(s.th,{children:"Purpose"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"ethos gateway start"})}),(0,r.jsx)(s.td,{children:"Multi-platform message gateway (Telegram, Slack, Discord, WhatsApp, email)."})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"ethos cron run"})}),(0,r.jsx)(s.td,{children:"Scheduled-job worker."})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"ethos serve"})}),(0,r.jsx)(s.td,{children:"Web UI plus HTTP API."})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:(0,r.jsx)(s.code,{children:"ethos acp"})}),(0,r.jsx)(s.td,{children:"Agent Control Protocol server for mesh coordination."})]})]})]}),"\n",(0,r.jsxs)(s.p,{children:["The examples below use ",(0,r.jsx)(s.code,{children:"ethos gateway start"}),". Substitute any of the others \u2014 the unit-file shape is the same."]}),"\n",(0,r.jsx)(s.h2,{id:"steps",children:"Steps"}),"\n",(0,r.jsx)(s.h3,{id:"1-foreground-test-first",children:"1. Foreground-test first"}),"\n",(0,r.jsx)(s.p,{children:"Daemons fail silently. Confirm the command works under your shell before wrapping it in a service manager."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ethos gateway start\n"})}),"\n",(0,r.jsxs)(s.p,{children:["Send your bot a test message from the target platform; confirm a reply. Press ",(0,r.jsx)(s.code,{children:"Ctrl+C"})," to stop. If foreground does not work, the daemon will not either \u2014 fix the config first."]}),"\n",(0,r.jsx)(s.p,{children:"Note the absolute path to the binary:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"which ethos\n"})}),"\n",(0,r.jsx)(s.p,{children:"You'll paste it into the unit file in the next step."}),"\n",(0,r.jsx)(s.h3,{id:"2a-macos--launchd",children:"2A. macOS \u2014 launchd"}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"launchd"})," ships with macOS. Unit files (plists) live in ",(0,r.jsx)(s.code,{children:"~/Library/LaunchAgents/"}),". Write ",(0,r.jsx)(s.code,{children:"~/Library/LaunchAgents/ai.ethosagent.gateway.plist"}),":"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-xml",children:'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"\n "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>ai.ethosagent.gateway</string>\n\n <key>ProgramArguments</key>\n <array>\n <string>/bin/sh</string>\n <string>-c</string>\n <string>/usr/local/bin/ethos gateway start; c=$?; if [ $c -eq 3 ] || [ $c -eq 78 ]; then exit 0; fi; exit $c</string>\n </array>\n\n <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>/usr/local/bin:/usr/bin:/bin</string>\n <key>HOME</key>\n <string>/Users/YOUR_USERNAME</string>\n </dict>\n\n <key>WorkingDirectory</key>\n <string>/Users/YOUR_USERNAME</string>\n\n <key>
1RunAtLoad</key>\n <true/>\n\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n\n <key>ThrottleInterval</key>\n <integer>30</integer>\n\n <key>StandardOutPath</key>\n <string>/Users/YOUR_USERNAME/.ethos/logs/gateway.out.log</string>\n\n <key>StandardErrorPath</key>\n <string>/Users/YOUR_USERNAME/.ethos/logs/gateway.err.log</string>\n</dict>\n</plist>\n'})}),"\n",(0,r.jsxs)(s.p,{children:["Replace ",(0,r.jsx)(s.code,{children:"YOUR_USERNAME"})," with the output of ",(0,r.jsx)(s.code,{children:"whoami"})," and the binary path with ",(0,r.jsx)(s.code,{children:"which ethos"}),". Then load and start:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"launchctl load ~/Library/LaunchAgents/ai.ethosagent.gateway.plist\nlaunchctl start ai.ethosagent.gateway\nlaunchctl list | grep ethosagent\ntail -f ~/.ethos/logs/gateway.out.log\n"})}),"\n",(0,r.jsx)(s.p,{children:"Stop, unload, or reload:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"launchctl stop ai.ethosagent.gateway\nlaunchctl unload ~/Library/LaunchAgents/ai.ethosagent.gateway.plist\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"RunAtLoad"})," plus the ",(0,r.jsx)(s.code,{children:"~/Library/LaunchAgents/"})," location starts the agent at login. ",(0,r.jsx)(s.code,{children:"KeepAlive"})," with ",(0,r.jsx)(s.code,{children:"SuccessfulExit"})," set to ",(0,r.jsx)(s.code,{children:"false"})," restarts it after a non-zero exit (a crash) and leaves it stopped after a clean exit."]}),"\n",(0,r.jsxs)(s.p,{children:["launchd has no equivalent of systemd's ",(0,r.jsx)(s.code,{children:"RestartPreventExitStatus"}),", so the ",(0,r.jsx)(s.code,{children:"/bin/sh -c"})," wrapper does that job. The gateway exits ",(0,r.jsx)(s.code,{children:"78"})," when ",(0,r.jsx)(s.code,{children:"config.yaml"})," is invalid or every bot's credentials were refused, and ",(0,r.jsx)(s.code,{children:"3"})," when another gateway already holds the state directory. Both are refusals that a restart cannot fix. The wrapper turns them into exit ",(0,r.jsx)(s.code,{children:"0"}),", so launchd leaves the job stopped instead of respawning it forever. The reason stays in ",(0,r.jsx)(s.code,{children:"gateway.err.log"}),"."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"ThrottleInterval"})," is the minimum gap, in seconds, between two launches of the job. launchd's default is 10. A value of 30 caps a crash loop at two starts a minute, which keeps a failing provider or platform from being hammered."]}),"\n",(0,r.jsx)(s.h3,{id:"2b-linux--systemd-user-unit",children:"2B. Linux \u2014 systemd user unit"}),"\n",(0,r.jsxs)(s.p,{children:["User units live in ",(0,r.jsx)(s.code,{children:"~/.config/systemd/user/"})," and run as your login user. Write ",(0,r.jsx)(s.code,{children:"~/.config/systemd/user/ethos-gateway.service"}),":"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-ini",children:"[Unit]\nDescription=Ethos gateway\nAfter=network-online.target\nWants=network-online.target\nStartLimitIntervalSec=300\nStartLimitBurst=5\n\n[Service]\nType=notify\nExecStart=/usr/bin/ethos gateway start\nRestart=on-failure\nRestartSec=5\nRestartPreventExitStatus=3 78\nWatchdogSec=30\nStandardOutput=append:%h/.ethos/logs/gateway.out.log\nStandardError=append:%h/.ethos/logs/gateway.err.log\nEnvironment=NODE_ENV=production\n\n[Install]\nWantedBy=default.target\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"Type=notify"})," holds ",(0,r.jsx)(s.code,{children:"systemctl start"})," until the gateway has started its adapters and sent ",(0,r.jsx)(s.code,{children:"READY=1"})," (",(0,r.jsx)(s.code,{children:"notifyReady"})," in ",(0,r.jsx)(s.code,{children:"apps/ethos/src/sd-notify.ts"}),"). ",(0,r.jsx)(s.code,{children:"WatchdogSec=30"})," restarts a gateway whose event loop stops sending the keep-alive ",(0,r.jsx)(s.code,{children:"startWatchdog"})," schedules at half that interval \u2014 the alive-but-wedged case a crash restart never sees."]}),"\n",(0,r.jsx)(s.p,{children:"Alternatively, generate the unit file automatically:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ethos systemd-unit ethos-gateway > ~/.config/systemd/user/ethos-gateway.service\n"})}),"\n",(0,r.jsxs)(s.p,{children:["This generates a production-ready unit with managed mode (",(0,r.jsx)(s.code,{children:"ETHOS_MANAGED=1"}),") and ",(0,r.jsx)(s.code,{children:"EnvironmentFile"})," for secrets. See ",(0,r.jsx)(s.a,{href:"/docs/using/reference/cli#ethos-systemd-unit",children:"CLI reference: systemd-unit"})," for placeholders and customisation."]}),"\n",(0,r.jsxs)(s.p,{children:["Replace ",(0,r.jsx)(s.code,{children:"/u
1sr/bin/ethos"})," with the output of ",(0,r.jsx)(s.code,{children:"which ethos"})," if writing the unit manually. Enable, start, and inspect:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"systemctl --user daemon-reload\nsystemctl --user enable --now ethos-gateway.service\nsystemctl --user status ethos-gateway\njournalctl --user -u ethos-gateway -f\n"})}),"\n",(0,r.jsx)(s.p,{children:"To survive logout on a headless server:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo loginctl enable-linger $USER\n"})}),"\n",(0,r.jsx)(s.p,{children:"Restart, stop, or disable:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"systemctl --user restart ethos-gateway\nsystemctl --user stop ethos-gateway\nsystemctl --user disable ethos-gateway\n"})}),"\n",(0,r.jsx)(s.h3,{id:"2c-cross-platform--pm2",children:"2C. Cross-platform \u2014 pm2"}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.a,{href:"https://pm2.keymetrics.io",children:"pm2"})," is a Node process manager. Same commands on macOS, Linux, and Windows; bundled log rotation; ",(0,r.jsx)(s.code,{children:"pm2 startup"})," wires into the OS service manager."]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"npm install -g pm2\npm2 start ethos --name ethos-gateway -- gateway start\npm2 list\npm2 logs ethos-gateway\n"})}),"\n",(0,r.jsxs)(s.p,{children:["The ",(0,r.jsx)(s.code,{children:"--"})," separates pm2's own flags from the args passed to ",(0,r.jsx)(s.code,{children:"ethos"}),". Survive reboots:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"pm2 startup # prints a command \u2014 run it as root\npm2 save\n"})}),"\n",(0,r.jsx)(s.p,{children:"Common operations:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"pm2 restart ethos-gateway\npm2 stop ethos-gateway\npm2 delete ethos-gateway\npm2 monit\npm2 logs ethos-gateway --lines 200\n"})}),"\n",(0,r.jsxs)(s.p,{children:["Running gateway + serve together as one supervised unit? Don't list them as separate PM2 apps \u2014 use ",(0,r.jsx)(s.a,{href:"/docs/using/how-to/deploy-in-production",children:"Deploy in production"}),", which wraps ",(0,r.jsx)(s.code,{children:"ethos run-all"})," as a single PM2 process that spawns and watches both children. That gives you in-process restart-on-crash AND PM2's reboot survival without the double-supervision footgun."]}),"\n",(0,r.jsxs)(s.p,{children:["The pm2-app-per-ethos-command shape ",(0,r.jsx)(s.em,{children:"is"})," still right when you want to daemonise just one long-running command (e.g. only ",(0,r.jsx)(s.code,{children:"ethos cron run"})," for a scheduled-job worker without the gateway), which is what this page is about."]}),"\n",(0,r.jsx)(s.h3,{id:"2d-windows--task-scheduler",children:"2D. Windows \u2014 Task Scheduler"}),"\n",(0,r.jsxs)(s.p,{children:["Task Scheduler ships with Windows and needs no admin rights for a task that runs as you at logon. A small PowerShell supervisor restarts the gateway after a crash and stops on the two refusal exits (",(0,r.jsx)(s.code,{children:"3"}),", ",(0,r.jsx)(s.code,{children:"78"}),"), the same rule as the systemd unit's ",(0,r.jsx)(s.code,{children:"RestartPreventExitStatus"}),". Task Scheduler starts that supervisor at logon."]}),"\n",(0,r.jsxs)(s.p,{children:["Save this as ",(0,r.jsx)(s.code,{children:"%USERPROFILE%\\.ethos\\gateway-supervisor.ps1"}),":"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-powershell",children:'$ethos = "$env:LOCALAPPDATA\\ethos\\bin\\ethos.cmd"\n$log = "$env:USERPROFILE\\.ethos\\logs\\gateway.log"\nNew-Item -ItemType Directory -Force -Path (Split-Path $log) | Out-Null\nwhile ($true) {\n & $ethos gateway start *>> $log\n $code = $LASTEXITCODE\n # 0 = clean stop; 3 = another gateway holds the state dir; 78 = invalid config or refused tokens.\n if ($code -eq 0 -or $code -eq 3 -or $code -eq 78) { exit $code }\n Start-Sleep -Seconds 30\n}\n'})}),"\n",(0,r.jsxs)(s.p,{children:["If you did not use the Windows installer, replace ",(0,r.jsx)(s.code,{children:"$ethos"})," with the output of ",(0,r.jsx)(s.code,{children:"(Get-Command ethos).Source"}),". Register and start the task from PowerShell:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-powershell",children:"$action = New-ScheduledTaskAction -Execute 'powershell.exe' `\n -Argument \"-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `\"$env:USERPROFILE\\.ethos\\gateway-supervisor.ps1`\"\"\n$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME\n$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) `\n -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `\n -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries\nRegister-ScheduledTask -TaskName 'Ethos gateway' -Action $action -Trigger $trigger -Settings $
1settings\nStart-ScheduledTask -TaskName 'Ethos gateway'\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"-ExecutionTimeLimit ([TimeSpan]::Zero)"})," matters: without it, Task Scheduler stops the task after its default limit of three days. ",(0,r.jsx)(s.code,{children:"-RestartCount"})," and ",(0,r.jsx)(s.code,{children:"-RestartInterval"})," are Task Scheduler's own restart-on-failure, which retries the supervisor if the task itself fails. Crash restarts of the gateway are the supervisor loop's job, because it can tell a crash from a refusal by exit code."]}),"\n",(0,r.jsx)(s.p,{children:"Stop, start, or remove:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-powershell",children:"Stop-ScheduledTask -TaskName 'Ethos gateway'\nStart-ScheduledTask -TaskName 'Ethos gateway'\nUnregister-ScheduledTask -TaskName 'Ethos gateway' -Confirm:$false\n"})}),"\n",(0,r.jsxs)(s.p,{children:["pm2 (",(0,r.jsx)(s.a,{href:"#2c-cross-platform--pm2",children:"2C"}),") also runs on Windows. It needs a third-party package such as ",(0,r.jsx)(s.code,{children:"pm2-windows-startup"})," to survive a reboot, because ",(0,r.jsx)(s.code,{children:"pm2 startup"})," does not support Windows."]}),"\n",(0,r.jsxs)(s.h3,{id:"3-update-the-daemon-after-ethos-upgrade",children:["3. Update the daemon after ",(0,r.jsx)(s.code,{children:"ethos upgrade"})]}),"\n",(0,r.jsx)(s.p,{children:"The running process keeps the old binary in memory. Always restart after upgrading:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ethos upgrade\nlaunchctl stop ai.ethosagent.gateway && launchctl start ai.ethosagent.gateway # macOS\nsystemctl --user restart ethos-gateway # Linux\npm2 restart ethos-gateway # pm2\n"})}),"\n",(0,r.jsxs)(s.p,{children:["On Windows, run ",(0,r.jsx)(s.code,{children:"Stop-ScheduledTask -TaskName 'Ethos gateway'"})," then ",(0,r.jsx)(s.code,{children:"Start-ScheduledTask -TaskName 'Ethos gateway'"}),"."]}),"\n",(0,r.jsx)(s.h2,{id:"verify",children:"Verify"}),"\n",(0,r.jsx)(s.p,{children:"The bot replies to a fresh message within ten seconds, and the appropriate liveness check passes:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:'launchctl list | grep ai.ethosagent # macOS \u2014 non-empty line\nsystemctl --user is-active ethos-gateway # Linux \u2014 prints "active", exit 0\npm2 jlist | jq \'.[] | .name\' # pm2 \u2014 includes "ethos-gateway"\n'})}),"\n",(0,r.jsxs)(s.p,{children:["On Windows, ",(0,r.jsx)(s.code,{children:"(Get-ScheduledTask -TaskName 'Ethos gateway').State"})," prints ",(0,r.jsx)(s.code,{children:"Running"}),"."]}),"\n",(0,r.jsx)(s.p,{children:"Tail the structured logs Ethos writes alongside whatever stdout your service manager captures:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"tail -f ~/.ethos/logs/gateway.out.log\n"})}),"\n",(0,r.jsx)(s.h2,{id:"operator-concerns",children:"Operator concerns"}),"\n",(0,r.jsx)(s.h3,{id:"linger-headless-linux",children:"Linger (headless Linux)"}),"\n",(0,r.jsxs)(s.p,{children:["systemd user units are tied to login sessions. When the last session for a user closes \u2014 including SSH disconnects \u2014 systemd kills all user services by default. On a headless server with no GUI session, ",(0,r.jsx)(s.code,{children:"ethos"})," stops as soon as you close SSH."]}),"\n",(0,r.jsx)(s.p,{children:"The fix is a one-time command:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo loginctl enable-linger $USER\n"})}),"\n",(0,r.jsx)(s.p,{children:"This tells systemd to keep the user's service manager alive even with zero sessions. The user's services start at boot and survive logout."}),"\n",(0,r.jsx)(s.p,{children:"To undo (e.g. when decommissioning):"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo loginctl disable-linger $USER\n"})}),"\n",(0,r.jsx)(s.p,{children:"Check the current state:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"loginctl show-user $USER --property=Linger\n"})}),"\n",(0,r.jsx)(s.h3,{id:"detached-child-processes",children:"Detached child processes"}),"\n",(0,r.jsxs)(s.p,{children:["The bash and process tools can spawn long-running background processes with ",(0,r.jsx)(s.code,{children:"detached: true"}),". These processes intentionally survive when ",(0,r.jsx)(s.code,{children:"ethos"})," itself stops \u2014 they are children of PID 1, not of the Ethos process tree. ",(0,r.jsx)(s.code,{children:"ethos stop"}),", ",(0,r.jsx)(s.code,{children:"systemctl --user stop ethos-gateway"}),", and ",(0,r.jsx)(s.code,{children:"SIGTERM"})," do not reap them."]}),"\n",(0,r.jsxs)(s.p,{children:["This is by design: a user might ask the agent to start a dev server or a build watcher that should keep running. But it means decommissioning ",(0,r.jsx)(s.code,{children:"ethos"})," does not automatically clean up everything it started."]}),"\n",(0,r.jsx)(s.p,{children:"To see what is still running:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ethos process list\n"})}),"\n",(0,r.jsx)(s.p,{children:"To stop a specific detached process:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ethos process stop <pid>\n"})}),"\n",(0,r.jsx)(s.p,{children:"To stop all tracked detached processes:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ethos process stop --all\n"})}),"\n",(0,r.jsxs)(s.p,{children:["If ",(0,r.jsx)(s.code,{children:"ethos process list"})," shows nothing but you suspect orphans, check for processes whose cwd is under ",(0,r.jsx)(s.code,{children:"~/.ethos/"}),":"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"lsof +D ~/.ethos/ 2>/dev/null | grep -v ethos\n"})}),"\n",(0,r.jsx)(s.h2,{id:"troubleshoot",children:"Troubleshoot"}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Daemon starts but the bot does not respond."})," \u2014 Run ",(0,r.jsx)(s.code,{children:"ethos gateway start"})," in your shell with the same ",(0,r.jsx)(s.code,{children:"~/.ethos/config.yaml"}),". If foreground works and daemon does not, it's almost always a stripped ",(0,r.jsx)(s.code,{children:"PATH"})," or ",(0,r.jsx)(s.code,{children:"HOME"}),". Hardcode the absolute path to ",(0,r.jsx)(s.code,{children:"ethos"})," in ",(0,r.jsx)(s.code,{children:"ProgramArguments"})," / ",(0,r.jsx)(s.code,{children:"ExecStart"})," and set ",(0,r.jsx)(s.code,{children:"HOME"})," explicitly (launchd)."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsxs)(s.strong,{children:[(0,r.jsx)(s.code,{children:"ethos: command not found"})," in the service log."]})," \u2014 Service managers do not source your shell rc. If you installed via ",(0,r.jsx)(s.code,{children:"nvm"}),", the binary lives at ",(0,r.jsx)(s.code,{children:"~/.nvm/versions/node/v24.x.x/bin/ethos"}),". Paste that absolute path into the unit file."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsxs)(s.strong,{children:[(0,r.jsx)(s.code,{children:"Run ethos setup first"})," on boot."]})," \u2014 ",(0,r.jsx)(s.code,{children:"HOME"})," does not point at your user account. systemd user units inherit it correctly; launchd sometimes does not. Set ",(0,r.jsx)(s.code,{children:"HOME"})," in the plist ",(0,r.jsx)(s.code,{children:"EnvironmentVariables"})," block as shown above."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsxs)(s.strong,{children:["The unit is ",(0,r.jsx)(s.code,{children:"failed"})," and systemd stopped restarting it."]})," \u2014 The gateway exits ",(0,r.jsx)(s.code,{children:"78"})," when ",(0,r.jsx)(s.code,{children:"~/.ethos/config.yaml"})," cannot be parsed, a bot binding points at nothing, or the platform refused every bot's token, and ",(0,r.jsx)(s.code,{children:"3"})," when another gateway already holds this state directory. Both are refusals, not crashes, so ",(0,r.jsx)(s.code,{children:"RestartPreventExitStatus=3 78"})," stops systemd retrying them. Run ",(0,r.jsx)(s.code,{children:"ethos doctor"})," for a config error, re-run ",(0,r.jsx)(s.code,{children:"ethos setup messaging"})," for a refused token (the log says ",(0,r.jsx)(s.code,{children:"failed permanently"}),"), or run ",(0,r.jsx)(s.code,{children:"ethos gateway status"})," for a held lock. Fix the cause, then run ",(0,r.jsx)(s.code,{children:"systemctl --user restart ethos-gateway"}),". ",(0,r.jsx)(s.code,{children:"StartLimitBurst=5"})," in ",(0,r.jsx)(s.code,{children:"StartLimitIntervalSec=300"})," likewise gives up after five crashes in five minutes; ",(0,r.jsx)(s.code,{children:"systemctl --user reset-failed ethos-gateway"})," clears it. A unit generated before these directives existed does not have them \u2014 regenerate it with ",(0,r.jsx)(s.code,{children:"ethos systemd-unit ethos-gateway"}),"."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"launchd keeps the job stopped after a config fix."})," \u2014 The wrapper mapped exit ",(0,r.jsx)(s.code,{children:"78"})," or ",(0,r.jsx)(s.code,{children:"3"})," to ",(0,r.jsx)(s.code,{children:"0"}),", so launchd treats the job as done. Run ",(0,r.jsx)(s.code,{children:"ethos doctor"})," (exit ",(0,r.jsx)(s.code,{children:"78"}),") or ",(0,r.jsx)(s.code,{children:"ethos gateway status"})," (exit ",(0,r.jsx)(s.code,{children:"3"}),"), fix the cause, then ",(0,r.jsx)(s.code,{children:"launchctl start ai.ethosagent.gateway"}),"."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsxs)(s.strong,{children:["The Windows task shows ",(0,r.jsx)(s.code,{children:"Ready"}),", not ",(0,r.jsx)(s.code,{children:"Running"}),"."]})," \u2014 The supervisor exited. Read the last lines of ",(0,r.jsx)(s.code,{children:"%USERPROFILE%\\.ethos\\logs\\gateway.log"}),": exit ",(0,r.jsx)(s.code,{children:"78"})," is a config error or refused bot tokens (",(0,r.jsx)(s.code,{children:"ethos doctor"}),", ",(0,r.jsx)(s.code,{children:"ethos setup messaging"}),"), exit ",(0,r.jsx)(s.code,{children:"3"})," is another gateway holding the state directory (",(0,r.jsx)(s.code,{children:"ethos gateway status"}),"). Fix it, then run ",(0,r.jsx)(s.code,{children:"Start-ScheduledTask -TaskName 'Ethos gateway'"}),". If the gateway keeps running after ",(0,r.jsx)(s.code,{children:"Stop-ScheduledTask"}),", stop it by command line: ",(0,r.jsx)(s.code,{children:"Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Where-Object CommandLine -like '*gateway start*' | ForEach-Object { Stop-Process -Id $_.ProcessId }"}),"."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Telegram returns HTTP 429."})," \u2014 Two gateway processes are polling the same bot token. Check for a duplicate launchd plist, a stale pm2 entry, or a forgotten ",(0,r.jsx)(s.code,{children:"tmux"})," session. One process per bot token."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Daemon stops on logout (Linux)."})," \u2014 Run ",(0,r.jsx)(s.code,{children:"sudo loginctl enable-linger $USER"})," once. Without it, systemd tears down user units when the last login session ends."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Memory grows unbounded."})," \u2014 Check ",(0,r.jsx)(s.code,{children:"pm2 monit"})," or ",(0,r.jsx)(s.code,{children:'top -p $(pgrep -f "ethos gateway")'}),". If the resident set climbs steadily over hours, it's likely a leak \u2014 file an issue with a ",(0,r.jsx)(s.code,{children:"node --inspect"})," heap snapshot. Short-term: pm2 supports ",(0,r.jsx)(s.code,{children:"--max-memory-restart 500M"})," to recycle the process at a threshold."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:"Logs missing on Linux."})," \u2014 ",(0,r.jsx)(s.code,{children:"StandardOutput=append:"})," requires systemd 240+; on older systems, drop those lines and use ",(0,r.jsx)(s.code,{children:"journalctl --user -u ethos-gateway"})," instead."]})]})}function h(e={}){let{wrapper:s}={...(0,a.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},7214(e,s,n){n.d(s,{R:()=>o,x:()=>i});var t=n(141);let r={},a=t.createContext(r);function o(e){let s=t.useContext(a);return t.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function i(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),t.createElement(a.Provider,{value:s},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.