diff --git a/loop/README.md b/loop/README.md index aa8ae25b..a85d0337 100644 --- a/loop/README.md +++ b/loop/README.md @@ -104,6 +104,18 @@ Read, Glob, and Grep remain unrestricted. | `ITERATION_DELAY` | `600` | Seconds between iterations when `ITERATION_COUNT` > 1. | | `PROMPT_FILE` | `loop/prompt.md` | Prompt content for Claude. | | `LOG_FILE` | `loop/loop.log` | Log output (gitignored). | +| `RATE_LIMIT_WAIT` | `3600` | Seconds to wait when rate limited (default 1 hour). | +| `MAX_RATE_LIMIT_RETRIES` | `5` | Max rate limit retries before scheduling launchd job. | + +## Rate Limit Handling + +The loop script automatically detects rate limits (429, quota exceeded, etc.) and handles them: + +1. **Inline retry** — On first rate limit hit, sleeps for `RATE_LIMIT_WAIT` seconds (default 1 hour) and retries. +2. **Escalating retries** — Retries up to `MAX_RATE_LIMIT_RETRIES` times with the same wait. +3. **launchd fallback** — After max retries, creates a self-cleaning launchd plist at `~/Library/LaunchAgents/com.aiui.overnight-retry.plist` that restarts the loop at the estimated reset time. The plist auto-removes after running. + +This means you can walk away knowing the automation will survive rate limits overnight. ## Scheduling (Optional) diff --git a/loop/loop.sh b/loop/loop.sh index 78c0de84..4f31c74c 100755 --- a/loop/loop.sh +++ b/loop/loop.sh @@ -1,6 +1,7 @@ #!/usr/bin/env sh # Headless loop script for overnight Claude Code automation. # Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete). +# Rate-limit aware: detects limits, sleeps until reset, and retries automatically. set -eu PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" @@ -9,18 +10,43 @@ LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}" ITERATION_COUNT="${ITERATION_COUNT:-1}" ITERATION_DELAY="${ITERATION_DELAY:-600}" CLAUDE_BIN="${CLAUDE_BIN:-claude}" +RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}" # Default: wait 1 hour on rate limit +MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}" # Max retries before giving up cd "$PROJECT_DIR" -echo "=== Overnight loop started $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" | tee -a "$LOG_FILE" -echo " PROMPT_FILE=$PROMPT_FILE" | tee -a "$LOG_FILE" -echo " CLAUDE_AUTONOMOUS=${CLAUDE_AUTONOMOUS:-0}" | tee -a "$LOG_FILE" -echo " ITERATION_COUNT=$ITERATION_COUNT" | tee -a "$LOG_FILE" -echo "" | tee -a "$LOG_FILE" +log() { + echo "$1" | tee -a "$LOG_FILE" +} + +# Check if plan has remaining tasks +plan_has_tasks() { + grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null +} + +# Detect rate limit from Claude output +check_rate_limit() { + # Check last 50 lines of log for rate limit indicators + tail -50 "$LOG_FILE" 2>/dev/null | grep -qi \ + -e "rate.limit" \ + -e "too.many.requests" \ + -e "429" \ + -e "quota.exceeded" \ + -e "usage.limit" \ + -e "limit.reached" 2>/dev/null +} + +log "=== Overnight loop started $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" +log " PROMPT_FILE=$PROMPT_FILE" +log " CLAUDE_AUTONOMOUS=${CLAUDE_AUTONOMOUS:-0}" +log " ITERATION_COUNT=$ITERATION_COUNT" +log " RATE_LIMIT_WAIT=${RATE_LIMIT_WAIT}s" +log "" i=1 +rate_limit_retries=0 while [ "$i" -le "$ITERATION_COUNT" ]; do - echo "--- Iteration $i/$ITERATION_COUNT @ $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ---" | tee -a "$LOG_FILE" + log "--- Iteration $i/$ITERATION_COUNT @ $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ---" export CLAUDE_PROJECT_DIR="$PROJECT_DIR" export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}" @@ -29,15 +55,88 @@ while [ "$i" -le "$ITERATION_COUNT" ]; do "$CLAUDE_BIN" -p --dangerously-skip-permissions --output-format=stream-json \ < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE" || true else - echo "Error: $PROMPT_FILE not found" | tee -a "$LOG_FILE" + log "Error: $PROMPT_FILE not found" exit 1 fi + # Check for rate limit after Claude exits + if check_rate_limit; then + rate_limit_retries=$((rate_limit_retries + 1)) + if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then + log "Rate limited $rate_limit_retries times — giving up." + log "Scheduling retry via launchd..." + + # Schedule a retry using launchd for after rate limit resets + PLIST_LABEL="com.aiui.overnight-retry" + PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist" + RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M') + RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1) + RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2) + + cat > "$PLIST_PATH" < + + + + Label + ${PLIST_LABEL} + ProgramArguments + + /bin/sh + -c + cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH} + + StartCalendarInterval + + Hour + ${RETRY_HOUR} + Minute + ${RETRY_MIN} + + EnvironmentVariables + + CLAUDE_AUTONOMOUS + 1 + CLAUDE_PROJECT_DIR + ${PROJECT_DIR} + PATH + /usr/local/bin:/usr/bin:/bin:$HOME/.local/bin + + StandardOutPath + ${LOG_FILE} + StandardErrorPath + ${LOG_FILE} + + +PLIST + + launchctl load "$PLIST_PATH" 2>/dev/null || true + log "Scheduled retry at ~${RETRY_TIME} via launchd ($PLIST_PATH)" + log "The plist auto-removes after running." + exit 0 + fi + + log "Rate limit detected (attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES)." + log "Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..." + sleep "$RATE_LIMIT_WAIT" + + # Check if plan still has tasks before retrying + if ! plan_has_tasks; then + log "All plan tasks completed during rate limit wait. Done." + break + fi + log "Retrying after rate limit..." + continue # Retry same iteration + fi + + # Reset rate limit counter on successful run + rate_limit_retries=0 + i=$((i + 1)) if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then - echo "Waiting ${ITERATION_DELAY}s before next iteration..." | tee -a "$LOG_FILE" + log "Waiting ${ITERATION_DELAY}s before next iteration..." sleep "$ITERATION_DELAY" fi done -echo "=== Loop complete $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" | tee -a "$LOG_FILE" +log "=== Loop complete $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" diff --git a/loop/plan.md b/loop/plan.md index 1ca42e7e..3f3fc599 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -1,4 +1,4 @@ -# Evening Run — YYYY-MM-DD +# Evening Run — 2026-03-03 > **Format**: Use `- [ ]` for incomplete, `- [x]` for complete. Enable autonomous mode with `CLAUDE_AUTONOMOUS=1` when running. @@ -10,7 +10,7 @@ Get the AIUI project to a green CI baseline: ESLint config, initial test coverag - [ ] Committed all work; `git status` clean - [ ] Pushed to remote (or at least backed up) -- [ ] Created branch for overnight: `git checkout -b overnight/YYYY-MM-DD` +- [ ] Created branch for overnight: `git checkout -b overnight/2026-03-03` ## Instructions for Claude diff --git a/loop/prompt.md b/loop/prompt.md index 94332822..a6fbc207 100644 --- a/loop/prompt.md +++ b/loop/prompt.md @@ -1 +1,9 @@ -Read `loop/plan.md` and work on the next incomplete task. Update the plan to mark completed items with `- [x]`. Add any relevant context to the Notes section for the next iteration. Do not stop until all tasks in the plan are complete. +Read loop/plan.md and work through the tasks in order. For each task: + +1. Mark it as in-progress by updating plan.md +2. Complete the work +3. Mark it as done (`- [x]`) in plan.md +4. Add any relevant notes to the Notes section +5. Move to the next unchecked task + +Follow CLAUDE.md conventions. Run `pnpm test`, `pnpm lint`, and `pnpm typecheck` after changes to verify nothing is broken. If a task is blocked, document why in the Notes section and move on. diff --git a/packages/app/dev-dist/sw.js b/packages/app/dev-dist/sw.js index 25a1fb77..c27d8884 100644 --- a/packages/app/dev-dist/sw.js +++ b/packages/app/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-cf23aef7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.3epgdnb2r7k" + "revision": "0.jv7f3n1n5ic" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {