#!/usr/bin/env bash # run-task.sh [cwd] # Quota-aware worker runner: survives 5-hour usage-limit caps by sleeping until # quota refreshes and resuming the SAME session instead of restarting from zero. set -u NAME="$1"; PROMPT_FILE="$2"; CWD="${3:-$HOME/bookshelf}" L="$HOME/bookshelf/logs"; mkdir -p "$L" LOG="$L/${NAME}.json"; ERR="$L/${NAME}.err"; SIDF="$L/${NAME}.sid"; ST="$L/${NAME}.state" MAX_WALL="${MAX_WALL:-86400}" # 24h total patience POLL="${POLL:-600}" # 10 min between quota probes MAX_HARD_FAILS="${MAX_HARD_FAILS:-3}" export JAVA_HOME="$HOME/toolchain/jdk21" export ANDROID_HOME="$HOME/toolchain/android-sdk" export ANDROID_SDK_ROOT="$ANDROID_HOME" export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH" export GRADLE_USER_HOME="$HOME/.gradle" cd "$CWD" || exit 1 # Stable session id so a killed run can be resumed rather than restarted. if [ ! -s "$SIDF" ]; then python3 -c "import uuid;print(uuid.uuid4())" > "$SIDF" FRESH=1 else FRESH=0 # sid pre-seeded (recovered) or left by an earlier attempt fi SID="$(cat "$SIDF")" say(){ echo "[$(date -Is)] $NAME: $*" >> "$ST"; } say "start sid=$SID fresh=$FRESH wall=${MAX_WALL}s poll=${POLL}s" CONT_PROMPT="Continue the task you were working on, from wherever you left off. \ Your original instructions are earlier in this conversation; re-read them and any \ files you already wrote before doing more work. Do not restart from scratch and do \ not redo completed work. Finish the task and give the final report." deadline=$(( $(date +%s) + MAX_WALL )) attempt=0; hard=0; quota_waits=0 while [ "$(date +%s)" -lt "$deadline" ]; do attempt=$((attempt+1)) if [ "$attempt" -eq 1 ] && [ "$FRESH" -eq 1 ]; then say "attempt $attempt: fresh (--session-id)" claude -p --model sonnet --permission-mode bypassPermissions \ --output-format json --add-dir "$HOME/bookshelf" \ --session-id "$SID" < "$PROMPT_FILE" > "$LOG" 2>"$ERR" else say "attempt $attempt: resume $SID" printf '%s' "$CONT_PROMPT" | claude -p --model sonnet \ --permission-mode bypassPermissions --output-format json \ --add-dir "$HOME/bookshelf" --resume "$SID" > "$LOG" 2>"$ERR" fi rc=$? blob="$(cat "$LOG" "$ERR" 2>/dev/null | head -c 20000)" # 1) quota / rate limit -> wait it out, do NOT burn a hard-fail if printf '%s' "$blob" | grep -qiE 'usage limit|limit will reset|limit resets|rate_limit_error|rate limit exceeded|429|too many requests|overloaded_error'; then quota_waits=$((quota_waits+1)) say "QUOTA hit (wait #$quota_waits). sleeping ${POLL}s then probing again." sleep "$POLL" continue fi # 2) session vanished -> start clean once if printf '%s' "$blob" | grep -qiE 'no conversation found|session not found|could not resume'; then say "session $SID unresumable; starting fresh" python3 -c "import uuid;print(uuid.uuid4())" > "$SIDF"; SID="$(cat "$SIDF")"; FRESH=1; attempt=0 continue fi # 3) success isErr="$(jq -r '.is_error // false' "$LOG" 2>/dev/null)" if [ "$rc" -eq 0 ] && [ "$isErr" != "true" ]; then say "SUCCESS after $attempt attempt(s), $quota_waits quota wait(s)" break fi # 4) genuine failure hard=$((hard+1)) say "hard failure #$hard (rc=$rc is_error=$isErr)" if [ "$hard" -ge "$MAX_HARD_FAILS" ]; then say "GIVING UP after $hard hard failures"; break; fi sleep 60 done [ "$(date +%s)" -ge "$deadline" ] && say "WALL CLOCK EXCEEDED" { echo "=== $NAME attempts=$attempt quota_waits=$quota_waits hard_fails=$hard ===" jq -r '"cost=$" + ((.total_cost_usd//0)|tostring) + " turns=" + ((.num_turns//0)|tostring) + " err=" + ((.is_error//"?")|tostring)' "$LOG" 2>/dev/null echo "--- result (tail) ---" jq -r '.result // "no result"' "$LOG" 2>/dev/null | tail -c 1800 } > "$L/${NAME}.summary"