#!/usr/bin/env bash # wave-guard.sh [...] # # Solves the real cause of the wave-3 loss: the sprite AUTO-SUSPENDS when idle # ("When idle, sprites pause automatically" — /.sprite/llm.txt). Detached workers # do NOT keep it awake; only services, live sessions, or a task lease do. # # Holds a sprite task lease (POST /v1/tasks, max expire 3600s) and renews it while # workers run, so the box stays hot with no console attached. When the workers stop # it writes the durable completion record AND releases the lease, so the sprite is # free to suspend instead of burning money idling. # # WAVE 4 POST-MORTEM — why this file was rewritten: # The previous lease_hold() did DELETE-then-POST with both results discarded, and # logged "lease renewed" unconditionally. If that POST ever failed, the box was left # with NO lease while the log claimed otherwise. That is exactly what wave 4 looks # like: last "renewal" 11:39, workers dead by 11:46, box rebooted 12:55. So now: # - every acquire is VERIFIED against GET /v1/tasks before it is believed # - a failed acquire retries, and is logged as a failure, never as a success # - the lease is re-checked every POLL (cheap), not only every RENEW, so a lease # lost between renewals is noticed in seconds instead of never set -u SENT="$1"; shift TASKS=("$@") L="$HOME/bookshelf/logs"; OUT="$L/$SENT"; LEASE="bookshelf-wave" RENEW="${RENEW:-900}" # proactively renew every 15 min against a 60 min lease POLL="${POLL:-30}" # but CHECK completion + lease health every 30s GUARD_LOG="$L/wave-guard.log" log() { echo "[$(date -Is)] $*" >> "$GUARD_LOG"; } lease_present() { sprite-env curl /v1/tasks 2>/dev/null | grep -q "\"$LEASE\""; } # Acquire and VERIFY. Returns 0 only if the lease is actually live afterwards. lease_acquire() { local attempt for attempt in 1 2 3; do sprite-env curl -X DELETE "/v1/tasks/$LEASE" >/dev/null 2>&1 sprite-env curl -X POST /v1/tasks -H 'Content-Type: application/json' \ -d "{\"name\":\"$LEASE\",\"expire\":\"3600s\"}" >/dev/null 2>&1 if lease_present; then return 0; fi log "WARNING: lease acquire attempt $attempt did not take; retrying" sleep 5 done return 1 } lease_release() { sprite-env curl -X DELETE "/v1/tasks/$LEASE" >/dev/null 2>&1; } trap 'lease_release; log "guard exiting, lease released"; exit 0' TERM INT log "guard start: sentinel=$SENT tasks=${TASKS[*]} renew=${RENEW}s poll=${POLL}s" if lease_acquire; then log "lease '$LEASE' acquired and VERIFIED (3600s)" else log "FATAL: could not acquire lease '$LEASE' -- the sprite may suspend mid-wave" fi last_renew=$(date +%s) while pgrep -f 'run-task\.sh|run-resume\.sh' >/dev/null; do sleep "$POLL" now=$(date +%s) if [ $(( now - last_renew )) -ge "$RENEW" ]; then if lease_acquire; then log "lease renewed and VERIFIED; workers still running" else log "ERROR: scheduled renewal FAILED; lease is not held" fi last_renew=$now elif ! lease_present; then # Lost between renewals (expiry race, API blip, someone else's DELETE). log "ERROR: lease disappeared between renewals -- re-acquiring now" if lease_acquire; then log "lease re-acquired and VERIFIED" last_renew=$now else log "ERROR: emergency re-acquire FAILED; sprite is unprotected" fi fi done log "workers stopped; writing $SENT" { echo "=== $SENT written $(date -Is) ===" echo "Workers finished. The orchestrator was NOT necessarily alive for this." echo for t in "${TASKS[@]}"; do echo "--- $t ---" grep -hE 'SUCCESS|GIVING UP|WALL CLOCK|QUOTA' "$L/$t.state" 2>/dev/null | tail -3 if [ -s "$L/$t.json" ]; then jq -r '"cost=$" + ((.total_cost_usd//0)|tostring) + " turns=" + ((.num_turns//0)|tostring)' "$L/$t.json" 2>/dev/null else echo "!! $t.json is 0 bytes -> worker was KILLED, not finished (hazard #3/#5)" fi echo done echo "NEXT: orchestrator must independently verify before accepting:" echo " cd ~/bookshelf && ./tasks/gw assembleDebug && ./tasks/gw testDebugUnitTest" echo " git status --porcelain # boundary check: who touched what" } > "$OUT" lease_release; log "lease released; sprite may suspend"