[object Object]

← back to Exo Keepalive

exo cluster keepalive: always-on launchd agent + portable node-bind script

eb7498a195ee19c67ba8af47abd80d0bf199e874 · 2026-07-06 20:22:14 -0700 · steve

Files touched

Diff

commit eb7498a195ee19c67ba8af47abd80d0bf199e874
Author: steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 20:22:14 2026 -0700

    exo cluster keepalive: always-on launchd agent + portable node-bind script
---
 bind-exo-node.sh | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 launchd.err.log  |  0
 launchd.out.log  |  0
 run-exo.sh       | 12 +++++++++
 4 files changed, 94 insertions(+)

diff --git a/bind-exo-node.sh b/bind-exo-node.sh
new file mode 100755
index 0000000..cdbd03d
--- /dev/null
+++ b/bind-exo-node.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+# bind-exo-node.sh — make THIS Mac Studio a permanent exo cluster node.
+# Portable: uses $HOME + the local user, so it works on any studio
+# (macstudio3 / stevestudio2 / steveabramsdesignsgmail.com / etc).
+# Run it ON the node you want to bind. Idempotent — safe to re-run.
+#
+#   Steve's rule: the exo cluster is ALWAYS on. This installs a launchd
+#   KeepAlive agent so exo (master+worker, API :52415) survives crash/logout/reboot
+#   and auto-joins the LAN cluster via exo's zero-config peer discovery.
+set -uo pipefail
+
+USER_HOME="$HOME"
+UV="$(command -v uv || echo "$USER_HOME/.local/bin/uv")"
+EXO_DIR="$USER_HOME/exo"
+KA_DIR="$USER_HOME/Projects/exo-keepalive"
+PLIST="$USER_HOME/Library/LaunchAgents/com.steve.exo-keepalive.plist"
+UID_N="$(id -u)"
+
+echo "== binding exo node: user=$(whoami) home=$USER_HOME =="
+
+# 0. sanity
+[ -x "$UV" ] || { echo "!! uv not found ($UV). Install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh"; exit 1; }
+[ -d "$EXO_DIR" ] || { echo "!! ~/exo not present. Clone it first: git clone <exo-repo> ~/exo"; exit 1; }
+
+# 1. dashboard build (exo hard-requires ~/exo/dashboard/build/index.html)
+if [ ! -f "$EXO_DIR/dashboard/build/index.html" ]; then
+  echo "== building dashboard (one-time; needs npm install/build) =="
+  ( cd "$EXO_DIR/dashboard" && npm install && npm run build ) || {
+    echo "!! dashboard build failed. If it's the npm allow-scripts gate, run:"
+    echo "   cd ~/exo/dashboard && npm approve-scripts --all && npm run build"; exit 1; }
+else
+  echo "== dashboard already built =="
+fi
+
+# 2. keepalive runner (portable)
+mkdir -p "$KA_DIR"
+cat > "$KA_DIR/run-exo.sh" <<EOF
+#!/usr/bin/env bash
+set -uo pipefail
+export HOME="$USER_HOME"
+export PATH="$USER_HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
+cd "$EXO_DIR" || exit 1
+echo "[\$(date +%FT%T%z)] === exo start (pid \$\$) ===" >> "$KA_DIR/exo.log"
+exec "$UV" run exo >> "$KA_DIR/exo.log" 2>&1
+EOF
+chmod +x "$KA_DIR/run-exo.sh"
+
+# 3. launchd plist (portable paths)
+mkdir -p "$USER_HOME/Library/LaunchAgents"
+cat > "$PLIST" <<EOF
+<?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>com.steve.exo-keepalive</string>
+  <key>ProgramArguments</key><array>
+    <string>/bin/bash</string><string>$KA_DIR/run-exo.sh</string>
+  </array>
+  <key>EnvironmentVariables</key><dict>
+    <key>HOME</key><string>$USER_HOME</string>
+    <key>PATH</key><string>$USER_HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
+  </dict>
+  <key>RunAtLoad</key><true/>
+  <key>KeepAlive</key><true/>
+  <key>ThrottleInterval</key><integer>15</integer>
+  <key>StandardOutPath</key><string>$KA_DIR/launchd.out.log</string>
+  <key>StandardErrorPath</key><string>$KA_DIR/launchd.err.log</string>
+</dict></plist>
+EOF
+
+# 4. (re)bootstrap
+launchctl bootout "gui/$UID_N/com.steve.exo-keepalive" 2>/dev/null || true
+launchctl bootstrap "gui/$UID_N" "$PLIST" 2>&1 || launchctl load -w "$PLIST" 2>&1
+launchctl kickstart -k "gui/$UID_N/com.steve.exo-keepalive" 2>/dev/null || true
+
+# 5. verify
+echo "== waiting for :52415 =="
+for i in $(seq 1 25); do
+  code=$(curl -s -o /dev/null -w "%{http_code}" -m3 http://localhost:52415/v1/models 2>/dev/null)
+  [ "$code" = "200" ] && { echo "✅ exo UP on $(hostname) :52415 — node bound, permanent across reboot"; exit 0; }
+  sleep 6
+done
+echo "⚠️ exo not up yet — check $KA_DIR/exo.log"; exit 1
diff --git a/launchd.err.log b/launchd.err.log
new file mode 100644
index 0000000..e69de29
diff --git a/launchd.out.log b/launchd.out.log
new file mode 100644
index 0000000..e69de29
diff --git a/run-exo.sh b/run-exo.sh
new file mode 100755
index 0000000..ea2da22
--- /dev/null
+++ b/run-exo.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# exo cluster keepalive runner — launched by com.steve.exo-keepalive (KeepAlive=true).
+# Starts the exo master+worker + OpenAI-compatible API at http://localhost:52415.
+# Steve's standing rule: the cluster is ALWAYS up. launchd respawns this if it exits.
+set -uo pipefail
+export HOME=/Users/macstudio3
+export PATH="/Users/macstudio3/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
+cd "$HOME/exo" || exit 1
+LOG="$HOME/Projects/exo-keepalive/exo.log"
+echo "[$(date +%FT%T%z)] === exo start (pid $$) ===" >> "$LOG"
+# exec so launchd tracks the real exo process (respawns on crash)
+exec /Users/macstudio3/.local/bin/uv run exo >> "$LOG" 2>&1

(oldest)  ·  back to Exo Keepalive  ·  exo: add /usr/sbin to PATH (networksetup); revert --extra ml 618139f →