feat(uno-q): one-command demo-node recovery after USB drop

recover.sh: re-tunnels, relaunches the supervisor with the cloud token in its
environment (env-only, read from ANTHROPIC_OAUTH_TOKEN — never on disk), starts the
matrix bridge app only if down, preserves a running llama to skip cold reload, and
verifies end-to-end (llama/daemon/bridge health + demo agent on claude-sonnet-5 +
matrix_pattern fires). Turns a mid-demo disconnect into a ~30s fix. Tokens are read
from the env, so the script carries no secrets.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-21 04:51:43 -07:00
co-authored by Claude Opus 4.8
parent b09068b60c
commit 3fef8c4d23
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# recover.sh — one-command recovery for the APESS Uno Q demo node after a USB drop.
#
# On a disconnect the daemon/llama/bridge die and the cloud token (env-only) is lost.
# This re-tunnels, relaunches the supervisor WITH the token in its environment,
# restarts the matrix bridge app, and verifies the whole chain end-to-end.
#
# Secrets are read from the environment — NEVER hardcoded here. Export first:
# export ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-... # required: cloud brain
# export NODE_TOKEN=zc_... # optional: end-to-end verify
# ./recover.sh
#
# Env knobs: SERIAL (default 65301572), the two tokens above.
set -u
SERIAL="${SERIAL:-65301572}"
A(){ adb -s "$SERIAL" "$@"; }
S(){ adb -s "$SERIAL" shell "$@"; }
ok(){ printf ' \033[32m✓\033[0m %s\n' "$*"; }
bad(){ printf ' \033[31m✗\033[0m %s\n' "$*"; }
step(){ printf '\n\033[1m%s\033[0m\n' "$*"; }
step "0· Preconditions"
if ! adb devices | grep -q "^${SERIAL}[[:space:]]*device"; then
bad "board $SERIAL not attached — re-plug USB, then re-run"; exit 1
fi
ok "board $SERIAL attached"
[ -n "${ANTHROPIC_OAUTH_TOKEN:-}" ] || { bad "ANTHROPIC_OAUTH_TOKEN not set — cloud brain will fail. export it and re-run"; exit 1; }
ok "cloud token present in env"
step "1· Tunnel"
A forward tcp:8080 tcp:8080 >/dev/null && ok "adb forward :8080 → laptop localhost:8080"
step "2· Stop stale supervisor + daemons (preserve llama)"
S 'for p in $(ps -C zeroclaw-supervisor -o pid= 2>/dev/null); do kill -9 $p 2>/dev/null; done
for p in $(ps -C zeroclaw -o pid= 2>/dev/null); do kill -9 $p 2>/dev/null; done
rm -f /home/arduino/.zc-supervisor.lock; sleep 2
echo " daemons left: $(ps -C zeroclaw -o pid= 2>/dev/null | wc -l)"'
step "3· Relaunch supervisor WITH token env (env-only, never on disk)"
S "export ANTHROPIC_OAUTH_TOKEN='$ANTHROPIC_OAUTH_TOKEN'; \
export ZEROCLAW_providers__models__anthropic__max__api_key='$ANTHROPIC_OAUTH_TOKEN'; \
setsid nohup /home/arduino/zeroclaw-supervisor.sh >/dev/null 2>&1 </dev/null & sleep 2; echo done" >/dev/null
S 'pgrep -f "[z]eroclaw-supervisor" >/dev/null' && ok "supervisor relaunched" || bad "supervisor did NOT start"
step "4· Matrix bridge app (start only if down)"
if [ "$(S 'printf "ping\n" | timeout 4 nc 127.0.0.1 9999 2>/dev/null')" = "pong" ]; then
ok "bridge already running"
else
S 'cd ~/ArduinoApps/uno-q-bridge && TMPDIR=/tmp arduino-app-cli app start ~/ArduinoApps/uno-q-bridge 2>&1 | tail -1'
fi
step "5· Wait for services"
for i in $(seq 1 30); do
L=$(S 'curl -sf -m3 http://127.0.0.1:8083/health >/dev/null 2>&1 && echo 1 || echo 0')
D=$(S 'curl -sf -m3 http://127.0.0.1:8080/health >/dev/null 2>&1 && echo 1 || echo 0')
printf '\r [%02d] llama=%s daemon=%s ' "$i" "$L" "$D"
[ "$D" = 1 ] && break; sleep 6
done; echo
[ "$L" = 1 ] && ok "llama :8083 healthy" || bad "llama :8083 DOWN (cold load can take 3–5 min; re-check)"
[ "$D" = 1 ] && ok "daemon :8080 healthy" || { bad "daemon :8080 DOWN"; exit 1; }
step "6· Bridge (matrix responder)"
P=$(S 'printf "ping\n" | timeout 4 nc 127.0.0.1 9999 2>/dev/null')
[ "$P" = "pong" ] && ok "bridge :9999 responds (ping→pong)" || bad "bridge :9999 not responding — re-run step 4"
step "7· End-to-end: demo agent = cloud sonnet + matrix fires"
if [ -n "${NODE_TOKEN:-}" ]; then
S 'printf "matrix 0\n" | timeout 5 nc 127.0.0.1 9999 >/dev/null 2>&1'
R=$(curl -s -m 30 -X POST "http://127.0.0.1:8080/webhook?agent=demo" \
-H "Authorization: Bearer $NODE_TOKEN" -H 'Content-Type: application/json' \
-d '{"message":"Show the rain animation on the LED matrix"}')
echo "$R" | grep -q "claude-sonnet-5" && ok "agent=demo on claude-sonnet-5" || bad "agent NOT on sonnet — token may not have loaded: $R"
M=$(S "docker logs --since 40s uno-q-bridge-main-1 2>&1 | grep -c \"parts=\['matrix', '1'\]\"")
[ "${M:-0}" -ge 1 ] && ok "matrix_pattern fired (rain)" || bad "matrix did not change"
else
echo " (NODE_TOKEN unset — skipping authenticated end-to-end check)"
fi
step "Recovery complete."
echo " Voice proxy (laptop): if it was running it auto-recovers via the re-armed tunnel."
echo " If not running: NODE_URL=http://127.0.0.1:8080 NODE_TOKEN=\$NODE_TOKEN python3 deploy/voice-client/serve.py 8090"