#!/usr/bin/env bash # Delete every mission and agent, and the memory they accumulated. # # Runs through the API rather than raw SQL, so missions go out by the same # path the UI uses and agents go through `batch-delete`'s FK-ordered # hard-purge (runtime → container → .brain → DB). Raw deletes leave # containers and brain files orphaned, which is how `container-reap-drift` # happened. # # WHAT SURVIVES, by FK rule and by design: # corpus_items SET NULL — the research seen-set, so a fresh harvest does # not re-download and re-note papers already covered # usage_events SET NULL for judge rows; agent rows are deleted explicitly # by `agents::hard_purge`, which overrides the FK # skills, repos, team_templates, nodes — untouched # # WHAT THIS CLEARS THAT IT USED NOT TO: the per-repo `.brain` files under # CLAWMATES_BRAIN_DIR. They are not in the database and no cascade reaches # them, so a wipe left every agent still recalling verdicts from missions # that no longer existed — measured 2026-09-22, an 18,982-byte repo brain # outliving its rows. "Clean slate" now means what it says. # # Do NOT run this while a deploy is mid-roll: it drives the API, and a # server recreate interrupts the calls. set -u HOST="${CLAWMATES_HOST:-gw-04}" OWNER="${CLAWMATES_OWNER_EMAIL:-om.sobh@gmail.com}" API="${CLAWMATES_API:-http://100.102.112.85:8088}" PG=clawmates_postgres_1 SERVER=clawmates_server_1 KEEP_BRAINS="${CLAWMATES_KEEP_BRAINS:-0}" psql_() { ssh "$HOST" "docker exec $PG psql -U postgres -d clawmates -tAc \"$1\""; } secret="wipe-$(openssl rand -hex 16)" hash=$(printf '%s' "$secret" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') rows=$(psql_ "insert into auth_sessions (user_id, token_hash, expires_at) select id, '$hash', now() + interval '20 minutes' from users where email='$OWNER' limit 1 returning 1;" 2>/dev/null | head -1 | tr -d '[:space:]') [ "$rows" = "1" ] || { echo "mint failed for $OWNER"; exit 1; } echo "== missions" for id in $(psql_ "select id from missions order by created_at" | tr -d '\r'); do code=$(ssh "$HOST" "curl -s -o /dev/null -w '%{http_code}' -X DELETE -H 'Authorization: Bearer $secret' '$API/api/missions/$id'") echo " $id -> $code" done echo "== agents (batch hard-purge, streamed)" ids=$(psql_ "select string_agg('\"'||id||'\"', ',') from agents" | tr -d '\r') if [ -n "$ids" ]; then ssh "$HOST" "curl -s -N -X POST -H 'Authorization: Bearer $secret' -H 'Content-Type: application/json' \ -d '{\"ids\":[$ids]}' '$API/api/claws/batch-delete'" \ | grep -oE '"stage":"[a-z_]+"' | sort | uniq -c | sed 's/^/ /' else echo " (none)" fi # The half no cascade reaches. Per-agent brains should already be gone with # their agents; the per-REPO ones belong to no agent and would otherwise # survive every wipe forever. if [ "$KEEP_BRAINS" = "1" ]; then echo "== brains (KEPT: CLAWMATES_KEEP_BRAINS=1)" ssh "$HOST" "docker exec $SERVER sh -c 'ls /data/brains/*.h5 2>/dev/null | wc -l'" | sed 's/^/ files left: /' else echo "== brains" ssh "$HOST" "docker exec $SERVER sh -c 'ls /data/brains/*.h5 2>/dev/null | wc -l'" | sed 's/^/ before: /' ssh "$HOST" "docker exec $SERVER sh -c 'rm -f /data/brains/*.h5 /data/brains/*.h5.onion 2>/dev/null; ls /data/brains/*.h5 2>/dev/null | wc -l'" | sed 's/^/ after: /' fi echo "== session" psql_ "delete from auth_sessions where token_hash='$hash' returning 1" | head -1 | sed 's/^/ removed: /' echo "== after" psql_ "select 'missions='||(select count(*) from missions)||' phases='||(select count(*) from mission_phases)||' events='||(select count(*) from mission_events)||' artifacts='||(select count(*) from mission_artifacts)||' evals='||(select count(*) from mission_phase_evaluations)||' agents='||(select count(*) from agents)||' episodes='||(select count(*) from podcast_episodes)||' usage='||(select count(*) from usage_events)||' corpus_KEPT='||(select count(*) from corpus_items)||' skills_KEPT='||(select count(*) from skills)" ssh "$HOST" "docker ps -a --format '{{.Names}}' | grep -c 'cm-runtime-mission' || true" | sed 's/^/ mission containers left: /'