Skip to content

Commit 21121d1

Browse files
committed
Add pre-launch rate-limit preflight for multi-account runs
1 parent a726e6d commit 21121d1

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed

configs/_common.sh

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,17 @@ REAL_HOME="$HOME"
467467
# are detected and skipped automatically in setup_multi_accounts.
468468
SKIP_ACCOUNTS="${SKIP_ACCOUNTS:-}"
469469

470+
# Rate-limit preflight probe before launching runs.
471+
# - RATE_LIMIT_PREFLIGHT=1 (default): probe each account with a tiny Claude request
472+
# - RATE_LIMIT_PREFLIGHT=0: disable probing
473+
# - RATE_LIMIT_PREFLIGHT_MODE=skip (default): drop rate-limited accounts from CLAUDE_HOMES
474+
# - RATE_LIMIT_PREFLIGHT_MODE=fail: abort launch if any account is rate-limited
475+
RATE_LIMIT_PREFLIGHT="${RATE_LIMIT_PREFLIGHT:-1}"
476+
RATE_LIMIT_PREFLIGHT_MODE="${RATE_LIMIT_PREFLIGHT_MODE:-skip}"
477+
RATE_LIMIT_PROBE_TIMEOUT_SEC="${RATE_LIMIT_PROBE_TIMEOUT_SEC:-20}"
478+
RATE_LIMIT_PROBE_MODEL="${RATE_LIMIT_PROBE_MODEL:-anthropic/claude-haiku-4-5-20251001}"
479+
RATE_LIMIT_PROBE_PROMPT="${RATE_LIMIT_PROBE_PROMPT:-Reply with exactly OK.}"
480+
470481
# Check whether an account's OAuth token is valid (or can be refreshed).
471482
# Args: $1 = account home directory (e.g., ~/.claude-homes/account1)
472483
# Returns 0 if the token is valid (or was successfully refreshed), 1 otherwise.
@@ -638,6 +649,123 @@ ensure_fresh_token_all() {
638649
export HOME="$REAL_HOME"
639650
}
640651

652+
# Probe a single account for immediate Anthropic rate-limit status.
653+
# Returns:
654+
# 0 => probe succeeded and account appears usable
655+
# 2 => account appears rate-limited
656+
# 1 => probe failed for non-rate-limit reasons (treated as usable with warning)
657+
_check_account_rate_limit() {
658+
local account_home=$1
659+
local output
660+
local rc
661+
662+
# timeout returns 124 on timeout
663+
output=$(
664+
HOME="$account_home" timeout "$RATE_LIMIT_PROBE_TIMEOUT_SEC" \
665+
claude --print \
666+
--output-format text \
667+
--permission-mode bypassPermissions \
668+
--model "$RATE_LIMIT_PROBE_MODEL" \
669+
"$RATE_LIMIT_PROBE_PROMPT" 2>&1
670+
)
671+
rc=$?
672+
673+
if echo "$output" | grep -qiE "rate[ _-]?limit|429|hit your limit|exceed your account'?s rate limit|too many requests"; then
674+
return 2
675+
fi
676+
677+
if [ "$rc" -eq 124 ]; then
678+
echo " Probe timed out (${RATE_LIMIT_PROBE_TIMEOUT_SEC}s); keeping account"
679+
return 1
680+
fi
681+
682+
if [ "$rc" -ne 0 ]; then
683+
# Non-rate-limit failure should not hard-block launches by default.
684+
# Keep account active and let run-time retry logic handle transient errors.
685+
echo " Probe command failed (exit $rc); keeping account"
686+
return 1
687+
fi
688+
689+
return 0
690+
}
691+
692+
# Check all active accounts for immediate rate-limit state and apply policy.
693+
# Must be called after setup_multi_accounts/ensure_fresh_token_all.
694+
preflight_rate_limits() {
695+
if [ "$RATE_LIMIT_PREFLIGHT" != "1" ]; then
696+
echo "Rate-limit preflight: disabled (RATE_LIMIT_PREFLIGHT=$RATE_LIMIT_PREFLIGHT)"
697+
return 0
698+
fi
699+
700+
if [ ${#CLAUDE_HOMES[@]} -eq 0 ]; then
701+
setup_multi_accounts
702+
fi
703+
704+
local kept_homes=()
705+
local limited_homes=()
706+
local warned=()
707+
708+
echo "Rate-limit preflight: probing ${#CLAUDE_HOMES[@]} account(s)..."
709+
710+
for home_dir in "${CLAUDE_HOMES[@]}"; do
711+
local label
712+
label=$(basename "$home_dir")
713+
echo " Checking $label ..."
714+
if _check_account_rate_limit "$home_dir"; then
715+
echo " OK"
716+
kept_homes+=("$home_dir")
717+
else
718+
local rl_rc=$?
719+
if [ "$rl_rc" -eq 2 ]; then
720+
echo " RATE-LIMITED"
721+
limited_homes+=("$home_dir")
722+
else
723+
warned+=("$home_dir")
724+
kept_homes+=("$home_dir")
725+
fi
726+
fi
727+
done
728+
729+
if [ ${#limited_homes[@]} -eq 0 ]; then
730+
echo "Rate-limit preflight: no limited accounts detected"
731+
return 0
732+
fi
733+
734+
echo "Rate-limit preflight: detected ${#limited_homes[@]} limited account(s):"
735+
for h in "${limited_homes[@]}"; do
736+
echo " - $(basename "$h")"
737+
done
738+
739+
case "$RATE_LIMIT_PREFLIGHT_MODE" in
740+
skip)
741+
CLAUDE_HOMES=("${kept_homes[@]}")
742+
if [ ${#CLAUDE_HOMES[@]} -eq 0 ]; then
743+
echo "ERROR: all accounts are currently rate-limited; aborting launch"
744+
return 1
745+
fi
746+
echo "Rate-limit preflight: continuing with ${#CLAUDE_HOMES[@]} account(s)"
747+
if [ "$PARALLEL_JOBS" -gt 0 ]; then
748+
local max_jobs=$(( SESSIONS_PER_ACCOUNT * ${#CLAUDE_HOMES[@]} ))
749+
if [ "$PARALLEL_JOBS" -gt "$max_jobs" ]; then
750+
echo "Rate-limit preflight: capping PARALLEL_JOBS from $PARALLEL_JOBS to $max_jobs"
751+
PARALLEL_JOBS=$max_jobs
752+
fi
753+
fi
754+
;;
755+
fail)
756+
echo "ERROR: aborting due to rate-limited accounts (RATE_LIMIT_PREFLIGHT_MODE=fail)"
757+
return 1
758+
;;
759+
*)
760+
echo "WARNING: unknown RATE_LIMIT_PREFLIGHT_MODE=$RATE_LIMIT_PREFLIGHT_MODE; defaulting to skip"
761+
CLAUDE_HOMES=("${kept_homes[@]}")
762+
[ ${#CLAUDE_HOMES[@]} -gt 0 ] || return 1
763+
;;
764+
esac
765+
766+
return 0
767+
}
768+
641769
# ============================================
642770
# PARALLEL TASK RUNNER
643771
# ============================================

configs/run_selected_tasks.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ if [ "$RUN_FULL" = true ] && [ -z "$SOURCEGRAPH_ACCESS_TOKEN" ]; then
184184
fi
185185

186186
ensure_fresh_token_all # also populates CLAUDE_HOMES[] via setup_multi_accounts
187+
preflight_rate_limits
187188

188189
# Auto-detect PARALLEL_TASKS: Daytona supports 125 concurrent sandboxes,
189190
# local Docker is limited by account sessions.
@@ -229,8 +230,13 @@ for task in selection['tasks']:
229230
continue
230231
if use_case_category_filter and task.get('use_case_category', '') != use_case_category_filter:
231232
continue
232-
# task_dir is relative to benchmarks/ in both formats
233-
task_dir = 'benchmarks/' + task['task_dir']
233+
# task_dir is relative to benchmarks/ in both formats.
234+
# Some historical selection entries only have task_id; fallback to
235+
# benchmarks/{suite}/{task_id} when task_dir is absent.
236+
task_rel = task.get('task_dir') or f\"{bm}/{task.get('task_id', '')}\"
237+
if not task_rel or task_rel.endswith('/'):
238+
continue
239+
task_dir = 'benchmarks/' + task_rel
234240
print(f'{bm}\t{task[\"task_id\"]}\t{task_dir}')
235241
"
236242
}

docs/DAYTONA.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ python3 scripts/build_daytona_registry.py
273273
| `DAYTONA_API_URL` | No | Override API endpoint (default: `https://app.daytona.io/api`) |
274274
| `DAYTONA_TARGET` | No | Override target region (default: `us`) |
275275
| `DAYTONA_OVERRIDE_STORAGE` | No | Override per-sandbox storage (MB). Set to `10240` to cap at Daytona's 10GB limit when tasks specify larger values in task.toml |
276+
| `RATE_LIMIT_PREFLIGHT` | No | Account preflight before launch (`1`=enabled, `0`=disabled; default `1`) |
277+
| `RATE_LIMIT_PREFLIGHT_MODE` | No | Behavior when account is limited: `skip` (default) or `fail` |
278+
| `RATE_LIMIT_PROBE_TIMEOUT_SEC` | No | Timeout in seconds for per-account probe (default `20`) |
276279

277280
## Troubleshooting
278281

@@ -284,6 +287,8 @@ python3 scripts/build_daytona_registry.py
284287

285288
**OAuth token expired**: Re-run `claude` in the account home directory to refresh, or switch to `--auth api-key`.
286289

290+
**Anthropic account is rate-limited**: launch scripts now run a preflight probe per account. Default behavior is `RATE_LIMIT_PREFLIGHT_MODE=skip`, which removes limited accounts from the run before `harbor run` starts. To hard-block launches instead, set `RATE_LIMIT_PREFLIGHT_MODE=fail`.
291+
287292
**MCP config errors**: Verify `SRC_ACCESS_TOKEN` is valid: `curl -H "Authorization: token $SRC_ACCESS_TOKEN" https://sourcegraph.com/.api/graphql`.
288293

289294
**Harbor + Daytona: "Sandbox not found"**: Usually a transient resource-contention error — Daytona could not allocate a sandbox when all slots were occupied. The retry logic in `_common.sh` will automatically re-queue these tasks with exponential backoff (up to 3 attempts). If it persists, check your Daytona tier limits or ensure `daytona-sdk` is installed in the same Python environment as Harbor.

0 commit comments

Comments
 (0)