feat: add circuit breaker to prevent Stop hook infinite loop
- Track block attempts per session in .attempts file - After 3 attempts (configurable via REVIEW_MAX_ATTEMPTS), trip breaker - Circuit breaker allows exit with warning instead of crashing - Clear attempts on approve or when breaker trips - Add 3 new tests for circuit breaker behavior (46 total) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9df3aedc2f
commit
0356ed237c
|
|
@ -40,24 +40,44 @@ get_state_file() {
|
|||
echo "${STATE_DIR}/${session_id}.json"
|
||||
}
|
||||
|
||||
# Circuit breaker settings
|
||||
MAX_BLOCK_ATTEMPTS="${REVIEW_MAX_ATTEMPTS:-3}"
|
||||
|
||||
# Commands
|
||||
cmd_check() {
|
||||
local session_id=$(get_session_id "${1:-}")
|
||||
local state_file=$(get_state_file "$session_id")
|
||||
local attempts_file="${STATE_DIR}/${session_id}.attempts"
|
||||
|
||||
# Check for stop_hook_active to prevent infinite loops
|
||||
# Claude Code passes JSON on stdin with this flag when we're in a continuation
|
||||
# Check for stop_hook_active (continuation after previous block)
|
||||
local hook_input=""
|
||||
local stop_hook_active="false"
|
||||
if [[ ! -t 0 ]]; then
|
||||
# stdin is not a terminal, try to read hook input
|
||||
hook_input=$(cat 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -n "$hook_input" ]]; then
|
||||
local stop_hook_active=$(echo "$hook_input" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
stop_hook_active=$(echo "$hook_input" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
fi
|
||||
|
||||
# Track block attempts for circuit breaker
|
||||
local attempts=0
|
||||
if [[ -f "$attempts_file" ]]; then
|
||||
attempts=$(cat "$attempts_file" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
# Circuit breaker: if we've hit max attempts, allow exit
|
||||
if [[ "$stop_hook_active" == "true" ]]; then
|
||||
# Already in a Stop hook continuation - allow exit to prevent infinite loop
|
||||
echo "Stop hook continuation detected - allowing exit to prevent loop"
|
||||
attempts=$((attempts + 1))
|
||||
echo "$attempts" > "$attempts_file"
|
||||
|
||||
if [[ $attempts -ge $MAX_BLOCK_ATTEMPTS ]]; then
|
||||
# Circuit breaker tripped
|
||||
echo "⚠ CIRCUIT BREAKER: Max block attempts ($MAX_BLOCK_ATTEMPTS) reached"
|
||||
echo "Review was not completed. Allowing exit to prevent infinite loop."
|
||||
echo "Session: $session_id"
|
||||
# Reset attempts for next time
|
||||
rm -f "$attempts_file"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
|
@ -124,6 +144,7 @@ EOF
|
|||
cmd_approve() {
|
||||
local session_id=$(get_session_id "${1:-}")
|
||||
local state_file=$(get_state_file "$session_id")
|
||||
local attempts_file="${STATE_DIR}/${session_id}.attempts"
|
||||
|
||||
if [[ ! -f "$state_file" ]]; then
|
||||
echo "No review state for session: $session_id"
|
||||
|
|
@ -140,6 +161,9 @@ cmd_approve() {
|
|||
}
|
||||
EOF
|
||||
|
||||
# Clear circuit breaker attempts
|
||||
rm -f "$attempts_file"
|
||||
|
||||
echo "✓ Review approved for session: $session_id"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -290,6 +290,34 @@ output=$(run_gate check reset-session)
|
|||
exit_code=$?
|
||||
assert_exit_code "Check blocks after re-enable" "2" "$exit_code"
|
||||
|
||||
# --- Circuit Breaker ---
|
||||
echo ""
|
||||
echo "## Circuit Breaker (Prevents Infinite Loop)"
|
||||
|
||||
# Enable a review
|
||||
run_gate enable test-circuit > /dev/null
|
||||
|
||||
# Simulate multiple stop_hook_active=true continuations
|
||||
# Default MAX_BLOCK_ATTEMPTS is 3
|
||||
echo '{"stop_hook_active": true}' | run_gate check test-circuit > /dev/null 2>&1 || true # attempt 1
|
||||
echo '{"stop_hook_active": true}' | run_gate check test-circuit > /dev/null 2>&1 || true # attempt 2
|
||||
|
||||
# Third attempt should trip circuit breaker
|
||||
output=$(echo '{"stop_hook_active": true}' | run_gate check test-circuit 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
assert_exit_code "Circuit breaker trips after max attempts" "0" "$exit_code"
|
||||
assert_output_contains "Circuit breaker message shown" "CIRCUIT BREAKER" "$output"
|
||||
|
||||
# Attempts file should be cleaned up
|
||||
if [[ ! -f "$TEST_STATE_DIR/test-circuit.attempts" ]]; then
|
||||
echo -e "${GREEN}PASS${NC}: Attempts file cleaned after circuit breaker"
|
||||
((PASSED++))
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}: Attempts file should be cleaned after circuit breaker"
|
||||
((FAILED++))
|
||||
fi
|
||||
|
||||
# --- Clean Command ---
|
||||
echo ""
|
||||
echo "## Clean Command"
|
||||
|
|
|
|||
Loading…
Reference in a new issue