feat(fleet): launcher heartbeat sidecar — HB for all runtimes (pi/claude/codex)
Replace the terminal `exec tmux` with a plain `tmux new-session -d` so the
launcher continues running after creating the pane. The script then resolves
the pane PID via `tmux list-panes -F '#{pane_pid}'` (with a brief retry loop)
and spawns a detached, runtime-agnostic heartbeat sidecar via `setsid bash -c
... &` + `disown`. The sidecar loops while `kill -0 <pane_pid>` succeeds,
writing ~/.config/mosaic/fleet/run/<AGENT>.hb atomically (tmp + mv) every
MOSAIC_HEARTBEAT_INTERVAL seconds (default 15), then exits naturally when the
runtime process dies — making `mosaic fleet ps` show stale then dead.
HB_RUN_DIR and interval are configurable via env; sidecar startup is
best-effort (failures warn but do not abort the launch). Two new shell tests
cover pane-PID resolution (test 6, real tmux) and sidecar invocation
correctness (test 7, fake-tmux + fake-setsid shims).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMoEx7hfdFGjUiCHuN1RRi
This commit is contained in:
@@ -6,6 +6,8 @@ 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
|
||||
@@ -96,5 +98,55 @@ else
|
||||
fi
|
||||
|
||||
mkdir -p "$MOSAIC_AGENT_WORKDIR"
|
||||
exec tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$AGENT_NAME" -c "$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
|
||||
|
||||
Reference in New Issue
Block a user