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"
|
echo "${STATE_DIR}/${session_id}.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Circuit breaker settings
|
||||||
|
MAX_BLOCK_ATTEMPTS="${REVIEW_MAX_ATTEMPTS:-3}"
|
||||||
|
|
||||||
# Commands
|
# Commands
|
||||||
cmd_check() {
|
cmd_check() {
|
||||||
local session_id=$(get_session_id "${1:-}")
|
local session_id=$(get_session_id "${1:-}")
|
||||||
local state_file=$(get_state_file "$session_id")
|
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
|
# Check for stop_hook_active (continuation after previous block)
|
||||||
# Claude Code passes JSON on stdin with this flag when we're in a continuation
|
|
||||||
local hook_input=""
|
local hook_input=""
|
||||||
|
local stop_hook_active="false"
|
||||||
if [[ ! -t 0 ]]; then
|
if [[ ! -t 0 ]]; then
|
||||||
# stdin is not a terminal, try to read hook input
|
|
||||||
hook_input=$(cat 2>/dev/null || true)
|
hook_input=$(cat 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$hook_input" ]]; then
|
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
|
if [[ "$stop_hook_active" == "true" ]]; then
|
||||||
# Already in a Stop hook continuation - allow exit to prevent infinite loop
|
attempts=$((attempts + 1))
|
||||||
echo "Stop hook continuation detected - allowing exit to prevent loop"
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -124,6 +144,7 @@ EOF
|
||||||
cmd_approve() {
|
cmd_approve() {
|
||||||
local session_id=$(get_session_id "${1:-}")
|
local session_id=$(get_session_id "${1:-}")
|
||||||
local state_file=$(get_state_file "$session_id")
|
local state_file=$(get_state_file "$session_id")
|
||||||
|
local attempts_file="${STATE_DIR}/${session_id}.attempts"
|
||||||
|
|
||||||
if [[ ! -f "$state_file" ]]; then
|
if [[ ! -f "$state_file" ]]; then
|
||||||
echo "No review state for session: $session_id"
|
echo "No review state for session: $session_id"
|
||||||
|
|
@ -140,6 +161,9 @@ cmd_approve() {
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Clear circuit breaker attempts
|
||||||
|
rm -f "$attempts_file"
|
||||||
|
|
||||||
echo "✓ Review approved for session: $session_id"
|
echo "✓ Review approved for session: $session_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,34 @@ output=$(run_gate check reset-session)
|
||||||
exit_code=$?
|
exit_code=$?
|
||||||
assert_exit_code "Check blocks after re-enable" "2" "$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 ---
|
# --- Clean Command ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "## Clean Command"
|
echo "## Clean Command"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue