feat(server): retire pm2 from operator path — launchd/systemd-user supervision + rotating logs - #271
Conversation
…pervision + rotating logs make prod/deploy now supervise the from-source server via the OS-native init system (launchd on macOS, systemd-user on Linux) through scripts/install-prod-service.sh, not pm2. Existing pm2-managed installs are auto-migrated on the next run (preserving PORT; NO_MIGRATE=1 to skip). ecosystem.config.cjs is removed. New CLI: autonomos logs (-f/--lines) and autonomos restart; autonomos stop is now service-aware (stops via the supervisor so launchd KeepAlive / systemd Restart don't revive it). The server owns a size-rotating ~/.autonomos/logs/autonomos.log (supervisor stdout -> /dev/null; stderr echoed to the boot backstop only on a TTY so it stays bounded). Option B (source-pointing unit) per ADR-050. Validated end-to-end on an isolated config dir + port 3199 (real launchd install/stop/restart/uninstall cycle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7yopZT2FmLCSboCAMAXxb
cf92171 to
d089079
Compare
nox-0x
left a comment
There was a problem hiding this comment.
Approving — the pm2 → launchd/systemd-user cutover is well-executed and the design comments do the heavy lifting of explaining the non-obvious bits (KeepAlive revival, server-owned rotating log to avoid the dual-writer / supervisor-held-fd trap, asymmetric stdout-always / stderr-TTY-only echo). Idempotent install script + smoke check + auto-migration is the right operator UX.
Two minor follow-up items (non-blocking):
-
🟢 The PR description claims migration "preserves PORT" from pm2, but
install-prod-service.shalways passes--port=$PORT(default$PROD_PORT=3100) tomigrate-from-pm2, which then short-circuits the detected-pm2-port branch incommands/migrate-from-pm2.ts:39-44. So a pm2 user running on a custom PORT (via.env) will be silently moved to 3100 bymake produnless they also setPROD_PORT. Either drop the unconditional--port=$PORTon the migration branch (let migrate-from-pm2 detect-and-preserve), or update the description/docs to be explicit that the shell PORT wins. -
🟢
autonomos stopis idempotent on Linux (systemctl --user stopreturns 0 if already stopped) but not on Mac (launchctl bootoutfails if the agent isn't loaded → exit 1, "Failed to stop the service"). Worth normalizing the "already-stopped" exit for parity. -
🟢
packages/server/src/logger.ts:126—getLogFilePath()is exported but unused (the CLI'slogs.tsreconstructs the path fromgetConfigDir()). Either wirelogs.tsto it or drop it.
None of these block merge.
Problem
Terry: "pm2 has been working but also very painful — I'd like something more mainstream and reliable that works for both mac and linux."
A 10-tool market study (Tailscale, Ollama, Caddy, code-server, Supabase, Syncthing, n8n + the agent-platform peers OpenClaw/Hermes/CMUX) found a unanimous norm: tools that ship a local daemon supervise it with the OS-native init system (systemd on Linux, launchd on macOS) — none use a Node process manager (pm2/forever). autonomOS was already on-norm at both ends —
autonomos install-service(shipped #170) writes a launchd LaunchAgent / systemd-user unit, andmake devruns foreground. The only off-norm leftover was pm2, surviving in the operator surface:ecosystem.config.cjs, theMakefile, and the remote-deploy SSH path. Since almost every existing user installs viamake prod/make deploy, that's exactly where the migration has to live.Solution (ADR-050, Option B)
make prod/make deploynow supervise the from-source server through the same OS-native unit the vended install uses — no pm2.graph TD subgraph OLD["❌ before — pm2"] A1["make prod"] --> A2["bun add -g pm2"] A2 --> A3["pm2 start ecosystem.config.cjs"] A3 --> A4["pm2 daemon :3100"] A4 -.->|"pm2 logs / restart / stop"| A5["pm2 CLI"] A4 -.->|"logs append forever, no rotation"| A6["/tmp/autonomos.log ♾️"] end subgraph NEW["✅ after — launchd / systemd-user"] B1["make prod"] --> B2["scripts/install-prod-service.sh"] B2 -->|"pm2 detected?"| B3["migrate-from-pm2 (preserve PORT)"] B2 --> B4["generate tsx wrapper (.autonomos-bin/)"] B4 --> B5["autonomos install-service --bin=wrapper"] B5 --> B6["launchd LaunchAgent / systemd-user unit"] B6 --> B7["daemon :3100 (KeepAlive / Restart=always)"] B7 -.->|"autonomos logs / restart / stop"| B8["autonomos CLI"] B7 -.->|"server-owned, size-rotating"| B9["~/.autonomos/logs/autonomos.log 🔁"] end OLD ==>|"auto-migrate on next make prod/deploy"| NEWWhat changed
scripts/install-prod-service.shreplacespm2 start ecosystem.config.cjs. Generates a tiny gitignored wrapper (exec tsx --env-file=.env packages/cli/src/index.ts "$@"— re-establishes the tsx loader +.envlikemake dev) and pointsinstall-service --bin=<wrapper>at it. Idempotent (re-runs reinstall + restart so new source goes live) and synchronous with a post-boot smoke check.autonomos→migrate-from-pm2(stop + deregister, preservePORT) with a loud announcement, before installing the unit.NO_MIGRATE=1escape hatch.install.shalready auto-migrates curl installs, so all paths converge.autonomos logs(-f/--lines) andautonomos restart.autonomos stopis now service-aware — underKeepAlive/Restart=alwaysa bare SIGTERM just gets revived, sostoptells the supervisor (launchctl bootout/systemctl --user stop) andrestartuseskickstart -kwith abootstrapfallback (sharedlib/service-control.ts).server/src/logger.ts): launchdStandardOutPath/ systemdappend:make the supervisor hold the log fd (unbounded, un-rotatable, two-writer-corrupting). So the server owns its log — tees stdout+stderr into a size-rotating~/.autonomos/logs/autonomos.log(sync fd appends; keep newest N). Echo is asymmetric: stdout always (carriesAUTONOMOS_READYIPC +--print-url; its supervisor sink is/dev/null), stderr only on a TTY (so theboot.error.logbackstop captures just the pre-attach window and stays bounded).autonomos logstails the rotating file.ecosystem.config.cjsdeleted;make deployno longer installs pm2; the systemd-user-over-sshXDG_RUNTIME_DIRgotcha is handled.Testing
/dev/nulltemplate redirect. Fullmake checkgreen — 612 server/cli + 227 dashboard tests.:3100):status/logs/stop.stopstayed down (no revive) →restart(bootstrap fallback) back up →uninstallclean. System verified clean afterward.autonomos.logvia the tee but not in the supervisor's stderr file (0 bytes) →boot.error.logstays bounded.Risks
make restartno longer rebuilds (it cycles the supervisor; usemake prodto pick up new source) — matches pm2-restart semantics, intentional.make deployafter this migrates forge off pm2 (preservingPORT=3100); subsequent deploys are clean rebuild-and-restart.Notes
install-servicefoundation stays — this finishes the migration it started.docs/DECISIONS.md; full research indocs/research/server-lifecycle-pm2-replacement.md.🤖 Generated with Claude Code