@@ -22,6 +22,7 @@ CB_NO_PROGRESS_THRESHOLD=${CB_NO_PROGRESS_THRESHOLD:-3} # Open circuit af
2222CB_SAME_ERROR_THRESHOLD=${CB_SAME_ERROR_THRESHOLD:- 5} # Open circuit after N loops with same error
2323CB_OUTPUT_DECLINE_THRESHOLD=${CB_OUTPUT_DECLINE_THRESHOLD:- 70} # Open circuit if output declines by >70%
2424CB_PERMISSION_DENIAL_THRESHOLD=${CB_PERMISSION_DENIAL_THRESHOLD:- 2} # Open circuit after N loops with permission denials (Issue #101)
25+ CB_READ_ONLY_TIMEOUT_THRESHOLD=${CB_READ_ONLY_TIMEOUT_THRESHOLD:- 2} # Open circuit after N consecutive read-only timeouts (#146)
2526CB_COOLDOWN_MINUTES=${CB_COOLDOWN_MINUTES:- 30} # Minutes before OPEN → HALF_OPEN auto-recovery (Issue #160)
2627CB_AUTO_RESET=${CB_AUTO_RESET:- false} # Reset to CLOSED on startup instead of waiting for cooldown
2728
@@ -52,6 +53,7 @@ init_circuit_breaker() {
5253 consecutive_no_progress: 0,
5354 consecutive_same_error: 0,
5455 consecutive_permission_denials: 0,
56+ consecutive_read_only_timeouts: 0,
5557 last_progress_loop: 0,
5658 total_opens: 0,
5759 reason: ""
@@ -92,6 +94,7 @@ init_circuit_breaker() {
9294 consecutive_no_progress: 0,
9395 consecutive_same_error: 0,
9496 consecutive_permission_denials: 0,
97+ consecutive_read_only_timeouts: 0,
9598 last_progress_loop: 0,
9699 total_opens: $total_opens,
97100 reason: "Auto-reset on startup"
@@ -155,6 +158,7 @@ record_loop_result() {
155158 local files_changed=$2
156159 local has_errors=$3
157160 local output_length=$4
161+ local was_read_only_timeout=${5:- false}
158162
159163 init_circuit_breaker
160164
@@ -163,14 +167,23 @@ record_loop_result() {
163167 local consecutive_no_progress=$( echo " $state_data " | jq -r ' .consecutive_no_progress' | tr -d ' [:space:]' )
164168 local consecutive_same_error=$( echo " $state_data " | jq -r ' .consecutive_same_error' | tr -d ' [:space:]' )
165169 local consecutive_permission_denials=$( echo " $state_data " | jq -r ' .consecutive_permission_denials // 0' | tr -d ' [:space:]' )
170+ local consecutive_read_only_timeouts=$( echo " $state_data " | jq -r ' .consecutive_read_only_timeouts // 0' | tr -d ' [:space:]' )
166171 local last_progress_loop=$( echo " $state_data " | jq -r ' .last_progress_loop' | tr -d ' [:space:]' )
167172
168173 # Ensure integers
169174 consecutive_no_progress=$(( consecutive_no_progress + 0 ))
170175 consecutive_same_error=$(( consecutive_same_error + 0 ))
171176 consecutive_permission_denials=$(( consecutive_permission_denials + 0 ))
177+ consecutive_read_only_timeouts=$(( consecutive_read_only_timeouts + 0 ))
172178 last_progress_loop=$(( last_progress_loop + 0 ))
173179
180+ # Track read-only timeouts (#146)
181+ if [[ " $was_read_only_timeout " == " true" ]]; then
182+ consecutive_read_only_timeouts=$(( consecutive_read_only_timeouts + 1 ))
183+ else
184+ consecutive_read_only_timeouts=0
185+ fi
186+
174187 # Detect progress from multiple sources:
175188 # 1. Files changed (git diff)
176189 # 2. Completion signal in response analysis (STATUS: COMPLETE or has_completion_signal)
@@ -249,6 +262,9 @@ record_loop_result() {
249262 if [[ $consecutive_permission_denials -ge $CB_PERMISSION_DENIAL_THRESHOLD ]]; then
250263 new_state=" $CB_STATE_OPEN "
251264 reason=" Permission denied in $consecutive_permission_denials consecutive loops"
265+ elif [[ $consecutive_read_only_timeouts -ge $CB_READ_ONLY_TIMEOUT_THRESHOLD ]]; then
266+ new_state=" $CB_STATE_OPEN "
267+ reason=" Read-only timeout in $consecutive_read_only_timeouts consecutive loops (agent stuck reading)"
252268 elif [[ $consecutive_no_progress -ge $CB_NO_PROGRESS_THRESHOLD ]]; then
253269 new_state=" $CB_STATE_OPEN "
254270 reason=" No progress detected in $consecutive_no_progress consecutive loops"
@@ -267,6 +283,9 @@ record_loop_result() {
267283 if [[ $consecutive_permission_denials -ge $CB_PERMISSION_DENIAL_THRESHOLD ]]; then
268284 new_state=" $CB_STATE_OPEN "
269285 reason=" Permission denied in $consecutive_permission_denials consecutive loops"
286+ elif [[ $consecutive_read_only_timeouts -ge $CB_READ_ONLY_TIMEOUT_THRESHOLD ]]; then
287+ new_state=" $CB_STATE_OPEN "
288+ reason=" Read-only timeout in $consecutive_read_only_timeouts consecutive loops (agent stuck reading)"
270289 elif [[ " $has_progress " == " true" ]]; then
271290 new_state=" $CB_STATE_CLOSED "
272291 reason=" Progress detected, circuit recovered"
@@ -305,6 +324,7 @@ record_loop_result() {
305324 --argjson consecutive_no_progress " $consecutive_no_progress " \
306325 --argjson consecutive_same_error " $consecutive_same_error " \
307326 --argjson consecutive_permission_denials " $consecutive_permission_denials " \
327+ --argjson consecutive_read_only_timeouts " $consecutive_read_only_timeouts " \
308328 --argjson last_progress_loop " $last_progress_loop " \
309329 --argjson total_opens " $total_opens " \
310330 --arg reason " $reason " \
@@ -315,6 +335,7 @@ record_loop_result() {
315335 consecutive_no_progress: $consecutive_no_progress,
316336 consecutive_same_error: $consecutive_same_error,
317337 consecutive_permission_denials: $consecutive_permission_denials,
338+ consecutive_read_only_timeouts: $consecutive_read_only_timeouts,
318339 last_progress_loop: $last_progress_loop,
319340 total_opens: $total_opens,
320341 reason: $reason,
@@ -441,6 +462,7 @@ reset_circuit_breaker() {
441462 consecutive_no_progress: 0,
442463 consecutive_same_error: 0,
443464 consecutive_permission_denials: 0,
465+ consecutive_read_only_timeouts: 0,
444466 last_progress_loop: 0,
445467 total_opens: 0,
446468 reason: $reason
0 commit comments