SPEC now states that a source which could not be reached must never be reported as a book that does not exist: Found / NotFound / Unavailable, where NotFound requires every source to have answered authoritatively. Also records that a rejected barcode must not be silent, and that the by-ISBN cover URL is not evidence a cover exists. run-task.sh exports CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0, per the wave-4 post-mortem: `claude -p` otherwise kills background tasks at 600s, so a worker that backgrounds a Gradle build can never report on it. Safe to edit now — no workers are running (hazard: never edit it while they are). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf
99 lines
4.0 KiB
Bash
Executable File
99 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# run-task.sh <task-name> <prompt-file> [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"
|
|
# Wave-4 post-mortem: `claude -p` terminates background tasks after 600s and ends
|
|
# the turn, so a worker that backgrounds a 10-minute Gradle build can never report
|
|
# on it. 0 = wait indefinitely. Worker prompts ALSO require foreground builds.
|
|
export CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0
|
|
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"
|