153 lines
5.9 KiB
Bash
Executable File
153 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
AGENT_NAME=${1:-${MOSAIC_AGENT_NAME:-}}
|
|
MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-mosaic-factory}
|
|
MOSAIC_AGENT_RUNTIME=${MOSAIC_AGENT_RUNTIME:-pi}
|
|
MOSAIC_AGENT_WORKDIR=${MOSAIC_AGENT_WORKDIR:-$HOME}
|
|
MOSAIC_AGENT_COMMAND=${MOSAIC_AGENT_COMMAND:-}
|
|
MOSAIC_HEARTBEAT_RUN_DIR=${MOSAIC_HEARTBEAT_RUN_DIR:-$HOME/.config/mosaic/fleet/run}
|
|
MOSAIC_HEARTBEAT_INTERVAL=${MOSAIC_HEARTBEAT_INTERVAL:-15}
|
|
|
|
if [ -z "$AGENT_NAME" ]; then
|
|
echo "ERROR: agent name argument or MOSAIC_AGENT_NAME is required" >&2
|
|
exit 64
|
|
fi
|
|
|
|
if ! command -v tmux >/dev/null 2>&1; then
|
|
echo "ERROR: tmux is required" >&2
|
|
exit 69
|
|
fi
|
|
|
|
if tmux -L "$MOSAIC_TMUX_SOCKET" has-session -t "=${AGENT_NAME}:0.0" 2>/dev/null; then
|
|
echo "Mosaic agent session already running: $AGENT_NAME on socket $MOSAIC_TMUX_SOCKET"
|
|
exit 0
|
|
fi
|
|
|
|
if [ -z "$MOSAIC_AGENT_COMMAND" ]; then
|
|
MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME"
|
|
fi
|
|
|
|
# ── Derive a runtime-bin PATH prefix ─────────────────────────────────────────
|
|
# Precedence:
|
|
# 1. $MOSAIC_RUNTIME_BIN (explicit override)
|
|
# 2. $(npm config get prefix)/bin (if npm is on PATH)
|
|
# 3. Fallbacks: $HOME/.npm-global/bin and $HOME/.local/bin
|
|
#
|
|
# Only directories that already exist are included. The prefix is baked into
|
|
# the pane command regardless of what the LAUNCHER process's $PATH contains,
|
|
# because the tmux pane inherits the tmux SERVER environment (not this script's
|
|
# environment). A dir on the launcher's PATH may be absent from the server PATH,
|
|
# so every existing candidate must always be included. Dedup within the
|
|
# constructed prefix avoids listing the same dir twice.
|
|
_build_runtime_bin_prefix() {
|
|
local candidates=()
|
|
|
|
if [ -n "${MOSAIC_RUNTIME_BIN:-}" ]; then
|
|
candidates+=("$MOSAIC_RUNTIME_BIN")
|
|
fi
|
|
|
|
if command -v npm >/dev/null 2>&1; then
|
|
local npm_prefix
|
|
npm_prefix=$(npm config get prefix 2>/dev/null) || true
|
|
if [ -n "$npm_prefix" ]; then
|
|
candidates+=("${npm_prefix}/bin")
|
|
fi
|
|
fi
|
|
|
|
candidates+=("$HOME/.npm-global/bin")
|
|
candidates+=("$HOME/.local/bin")
|
|
|
|
local prefix=""
|
|
for dir in "${candidates[@]}"; do
|
|
[ -d "$dir" ] || continue
|
|
if [ -z "$prefix" ]; then
|
|
prefix="$dir"
|
|
else
|
|
case ":${prefix}:" in
|
|
*":${dir}:"*) ;; # already in our prefix — skip
|
|
*) prefix="${prefix}:${dir}" ;;
|
|
esac
|
|
fi
|
|
done
|
|
|
|
printf '%s' "$prefix"
|
|
}
|
|
|
|
MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
|
|
|
|
# ── Build the pane command ────────────────────────────────────────────────────
|
|
# The pane command must:
|
|
# - Export the augmented PATH so the runtime binary is found.
|
|
# - exec the agent command so the runtime is the pane's foreground process
|
|
# (makes `fleet ps` pane_current_command check reliable; no DRIFT false-positive).
|
|
#
|
|
# Quoting strategy: single-quote the inner shell snippet so that variable
|
|
# references in MOSAIC_AGENT_COMMAND are NOT expanded here — they expand inside
|
|
# the pane shell. However, MOSAIC_RUNTIME_BIN_PREFIX and PATH must be expanded
|
|
# NOW (in this script) because the pane shell inherits the tmux server
|
|
# environment, not this script's env.
|
|
#
|
|
# We build the snippet as a double-quoted here-string embedded in a printf call
|
|
# to avoid nested quoting problems.
|
|
|
|
if [ -n "$MOSAIC_RUNTIME_BIN_PREFIX" ]; then
|
|
PANE_SHELL_SNIPPET="export PATH=\"${MOSAIC_RUNTIME_BIN_PREFIX}:\${PATH}\"; exec ${MOSAIC_AGENT_COMMAND}"
|
|
else
|
|
PANE_SHELL_SNIPPET="exec ${MOSAIC_AGENT_COMMAND}"
|
|
fi
|
|
|
|
mkdir -p "$MOSAIC_AGENT_WORKDIR"
|
|
|
|
# ── Launch the tmux session (no exec — we continue to wire the heartbeat) ────
|
|
tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" \
|
|
bash -c "$PANE_SHELL_SNIPPET"
|
|
|
|
# ── Resolve the pane PID (retry briefly to let the session initialise) ────────
|
|
PANE_PID=""
|
|
for _retry in 1 2 3 4 5; do
|
|
PANE_PID=$(tmux -L "$MOSAIC_TMUX_SOCKET" list-panes \
|
|
-t "=${AGENT_NAME}:0.0" -F '#{pane_pid}' 2>/dev/null || true)
|
|
[ -n "$PANE_PID" ] && break
|
|
sleep 0.2
|
|
done
|
|
|
|
# ── Spawn the heartbeat sidecar (detached, best-effort) ──────────────────────
|
|
# The sidecar writes ~/.config/mosaic/fleet/run/<AGENT>.hb atomically while the
|
|
# pane process is alive, then exits so the file goes stale (fleet ps shows stale
|
|
# then PANE=dead). It is runtime-agnostic: it only cares about the pane PID.
|
|
_start_heartbeat_sidecar() {
|
|
local agent="$1"
|
|
local pane_pid="$2"
|
|
local run_dir="$3"
|
|
local interval="$4"
|
|
local hb_file="${run_dir}/${agent}.hb"
|
|
|
|
mkdir -p "$run_dir"
|
|
|
|
# Write the sidecar as a self-contained bash one-liner so it carries no
|
|
# references to any variables from this script's environment.
|
|
local sidecar_script
|
|
sidecar_script=$(printf \
|
|
'hb=%s; pid=%s; iv=%s; mkdir -p "$(dirname "$hb")"; while kill -0 "$pid" 2>/dev/null; do tmp="$hb.tmp.$$"; printf "ts=%%s\npid=%%s\nstatus=ok\n" "$(date +%%Y-%%m-%%dT%%H:%%M:%%S%%z)" "$pid" > "$tmp" && mv "$tmp" "$hb"; sleep "$iv"; done' \
|
|
"$hb_file" "$pane_pid" "$interval")
|
|
|
|
# setsid + disown ensures the sidecar survives this script exiting.
|
|
# stderr/stdout go to /dev/null; failures are non-fatal.
|
|
if command -v setsid >/dev/null 2>&1; then
|
|
setsid bash -c "$sidecar_script" </dev/null >/dev/null 2>&1 &
|
|
else
|
|
bash -c "$sidecar_script" </dev/null >/dev/null 2>&1 &
|
|
fi
|
|
disown $! 2>/dev/null || true
|
|
}
|
|
|
|
if [ -n "$PANE_PID" ]; then
|
|
# Guard: do not let sidecar startup failures abort the launcher (set -e).
|
|
_start_heartbeat_sidecar "$AGENT_NAME" "$PANE_PID" \
|
|
"$MOSAIC_HEARTBEAT_RUN_DIR" "$MOSAIC_HEARTBEAT_INTERVAL" || \
|
|
echo "WARNING: heartbeat sidecar could not be started for $AGENT_NAME" >&2
|
|
else
|
|
echo "WARNING: could not resolve pane PID for $AGENT_NAME — heartbeat sidecar not started" >&2
|
|
fi
|