@@ -200,6 +200,7 @@ class StatusTracker:
200200 num_tasks_succeeded : int = 0
201201 num_tasks_failed : int = 0
202202 num_rate_limit_errors : int = 0
203+ num_connection_errors : int = 0
203204 num_api_errors : int = 0
204205 num_timeout_errors : int = 0
205206 num_other_errors : int = 0
@@ -1215,6 +1216,12 @@ def _rate_limit_decrement(concurrency_cap: int) -> int:
12151216 return max (2 , int (math .ceil (max (concurrency_cap * 0.4 , 3 ))))
12161217
12171218
1219+ def _connection_error_decrement (concurrency_cap : int ) -> int :
1220+ """Return a step-down size for connection errors matching rate-limit aggression."""
1221+
1222+ return _rate_limit_decrement (concurrency_cap )
1223+
1224+
12181225def _smooth_wait_based_cap (
12191226 current_cap : int ,
12201227 candidate_cap : int ,
@@ -2904,6 +2911,7 @@ async def get_all_responses(
29042911 quiet : bool = False ,
29052912 global_cooldown : int = 15 ,
29062913 rate_limit_window : float = 30.0 ,
2914+ connection_error_window : float = 30.0 ,
29072915 token_sample_size : int = 20 ,
29082916 status_report_interval : Optional [float ] = 120.0 ,
29092917 planning_rate_limit_buffer : float = PLANNING_RATE_LIMIT_BUFFER ,
@@ -2949,6 +2957,11 @@ async def get_all_responses(
29492957 throttling, while successful calls reset the counters and allow the pool to
29502958 scale back up.
29512959
2960+ Connection errors (e.g., transient network drops, Wi‑Fi/VPN instability, or bandwidth limitations)
2961+ are handled similarly: the helper tracks recent connection failures over
2962+ ``connection_error_window`` seconds and reduces parallelism when repeated
2963+ failures occur, while logging a hint to check network stability.
2964+
29522965 Timeout bursts now scale with the active level of parallelism. The helper
29532966 triggers a protective restart only after observing roughly 1.25× the
29542967 current parallel worker count worth of timeouts within
@@ -4201,9 +4214,13 @@ def _append_results(rows: List[Dict[str, Any]]) -> None:
42014214 successes_since_adjust = 0
42024215 active_workers = 0
42034216 rate_limit_window = max (1.0 , float (rate_limit_window ))
4217+ connection_error_window = max (1.0 , float (connection_error_window ))
42044218 rate_limit_error_times : Deque [float ] = deque ()
42054219 last_concurrency_scale_down = 0.0
42064220 last_concurrency_scale_up = 0.0
4221+ connection_error_times : Deque [float ] = deque ()
4222+ connection_errors_since_adjust = 0
4223+ last_connection_scale_down = 0.0
42074224 usage_samples : List [Tuple [int , int , int ]] = []
42084225 estimated_output_tokens = planning_output_tokens
42094226 estimated_output_tokens_per_prompt_live = float (planning_output_tokens )
@@ -4236,6 +4253,7 @@ def _effective_parallel_ceiling() -> int:
42364253 last_wait_adjust = 0.0
42374254 timeout_notes : Deque [str ] = deque (maxlen = 3 )
42384255 timeout_errors_since_last_status = 0
4256+ connection_errors_since_last_status = 0
42394257 first_timeout_logged = False
42404258 current_tokens_per_call = float (estimated_tokens_per_call )
42414259 last_rate_limit_concurrency_change = 0.0
@@ -4454,6 +4472,7 @@ def emit_parallelization_status(
44544472 context = "reporting parallelization status" ,
44554473 )
44564474 timeout_text = ""
4475+ connection_text = ""
44574476 total_completed = processed
44584477 if status .num_timeout_errors or total_completed :
44594478 denom = max (total_completed , 1 )
@@ -4475,6 +4494,10 @@ def emit_parallelization_status(
44754494 total_cost , sampled = cost_snapshot
44764495 cost_text = f"cost_so_far={ '~' if sampled else '' } ${ total_cost :.2f} "
44774496 prefix = f"[{ label } ] " if label else ""
4497+ if status .num_connection_errors or connection_errors_since_last_status :
4498+ connection_text = f"connection_errors={ status .num_connection_errors } "
4499+ if status_report_interval is not None and connection_errors_since_last_status :
4500+ connection_text += f" (+{ connection_errors_since_last_status } since last)"
44784501 status_bits : List [str ] = [
44794502 f"cap={ concurrency_cap } " ,
44804503 f"active={ active_workers } " ,
@@ -4490,6 +4513,8 @@ def emit_parallelization_status(
44904513 status_bits .append (ppm_piece )
44914514 if timeout_text :
44924515 status_bits .append (timeout_text )
4516+ if connection_text :
4517+ status_bits .append (connection_text )
44934518 msg = f"{ prefix } { timestamp } | { reason_clean } : " + ", " .join (status_bits )
44944519 if message_verbose :
44954520 print (msg )
@@ -4754,12 +4779,50 @@ def maybe_adjust_concurrency() -> None:
47544779 successes_since_adjust = 0
47554780 rate_limit_errors_since_adjust = 0
47564781
4782+ def maybe_adjust_for_connection_errors () -> None :
4783+ nonlocal concurrency_cap , connection_errors_since_adjust , last_connection_scale_down
4784+ nonlocal last_rate_limit_concurrency_change
4785+ now = time .time ()
4786+ window_start = now - connection_error_window
4787+ while connection_error_times and connection_error_times [0 ] < window_start :
4788+ connection_error_times .popleft ()
4789+ recent_errors = len (connection_error_times )
4790+ error_window_threshold = max (3 , int (math .ceil (concurrency_cap * 0.06 )))
4791+ consecutive_threshold = max (2 , int (math .ceil (concurrency_cap * 0.04 )))
4792+ should_scale_down = False
4793+ if recent_errors >= error_window_threshold :
4794+ should_scale_down = True
4795+ elif connection_errors_since_adjust >= consecutive_threshold :
4796+ should_scale_down = True
4797+ if should_scale_down and (
4798+ (now - last_connection_scale_down ) >= max (1.0 , connection_error_window * 0.75 )
4799+ ):
4800+ decrement = _connection_error_decrement (concurrency_cap )
4801+ new_cap = max (1 , concurrency_cap - decrement )
4802+ if new_cap != concurrency_cap :
4803+ old_cap = concurrency_cap
4804+ concurrency_cap = new_cap
4805+ reason = (
4806+ f"[network recovery] Cutting workers from { old_cap } to { new_cap } "
4807+ f"after { recent_errors } connection errors in the last "
4808+ f"{ int (round (connection_error_window ))} s. "
4809+ "If this persists, check network stability or bandwidth limits, "
4810+ "or reduce `n_parallels`."
4811+ )
4812+ logger .warning (reason )
4813+ emit_parallelization_status (reason , force = True )
4814+ connection_errors_since_adjust = 0
4815+ connection_error_times .clear ()
4816+ last_connection_scale_down = now
4817+ last_rate_limit_concurrency_change = now
4818+
47574819 async def worker () -> None :
47584820 nonlocal processed , call_count , nonlocal_timeout , active_workers , concurrency_cap , cooldown_until
47594821 nonlocal estimated_output_tokens , rate_limit_errors_since_adjust , successes_since_adjust , stop_event
47604822 nonlocal max_parallel_ceiling , last_wait_adjust , current_tokens_per_call , timeout_errors_since_last_status
47614823 nonlocal throughput_ceiling_ppm , observed_input_tokens_total , observed_output_tokens_total
4762- nonlocal observed_reasoning_tokens_total , observed_usage_count
4824+ nonlocal observed_reasoning_tokens_total , observed_usage_count , connection_errors_since_adjust
4825+ nonlocal connection_errors_since_last_status
47634826 while True :
47644827 if stop_event .is_set ():
47654828 break
@@ -4970,7 +5033,13 @@ async def _maybe_retry(backoff: float) -> bool:
49705033 if new_cap < 1 :
49715034 new_cap = 1
49725035 now = time .time ()
4973- safe_to_increase = (now - status .time_of_last_rate_limit_error ) >= rate_limit_window
5036+ last_connection_error = (
5037+ connection_error_times [- 1 ] if connection_error_times else 0.0
5038+ )
5039+ safe_to_increase = (
5040+ (now - status .time_of_last_rate_limit_error ) >= rate_limit_window
5041+ and (now - last_connection_error ) >= connection_error_window
5042+ )
49745043 if new_cap > concurrency_cap :
49755044 max_increase = max (1 , int (math .ceil (concurrency_cap * 0.12 )))
49765045 if not safe_to_increase :
@@ -5148,6 +5217,7 @@ async def _maybe_retry(backoff: float) -> bool:
51485217 status .num_tasks_succeeded += 1
51495218 successes_since_adjust += 1
51505219 rate_limit_errors_since_adjust = 0
5220+ connection_errors_since_adjust = 0
51515221 maybe_adjust_concurrency ()
51525222 _maybe_trigger_threshold_refresh ()
51535223 if processed % save_every_x_responses == 0 :
@@ -5241,13 +5311,26 @@ async def _maybe_retry(backoff: float) -> bool:
52415311 if error_detail :
52425312 base_message = f"{ base_message } : { error_detail } "
52435313 error_logs [ident ].append (error_detail )
5314+ if "connection error" in detail_lower :
5315+ status .num_connection_errors += 1
5316+ connection_errors_since_adjust += 1
5317+ connection_errors_since_last_status += 1
5318+ connection_error_times .append (time .time ())
5319+ maybe_adjust_for_connection_errors ()
5320+ _emit_first_error (
5321+ f"{ base_message } . This can indicate network instability or bandwidth limitations." ,
5322+ dedup_key = ("connection-error" , error_detail or None ),
5323+ )
52445324 error_key : Hashable = (
52455325 "non-timeout-error" ,
52465326 type (e ).__name__ ,
52475327 error_detail or None ,
52485328 )
5249- _emit_first_error (base_message , dedup_key = error_key )
5250- logger .warning (base_message )
5329+ if "connection error" not in detail_lower :
5330+ _emit_first_error (base_message , dedup_key = error_key )
5331+ logger .warning (base_message )
5332+ else :
5333+ logger .debug (base_message )
52515334 if attempts_left - 1 > 0 :
52525335 backoff = random .uniform (1 , 2 ) * (2 ** (max_retries - attempts_left ))
52535336 await _maybe_retry (backoff )
@@ -5412,9 +5495,17 @@ async def _maybe_retry(backoff: float) -> bool:
54125495 except APIConnectionError as e :
54135496 inflight .pop (ident , None )
54145497 status .num_api_errors += 1
5415- logger .warning (f"Connection error for { ident } : { e } " )
5416- _emit_first_error (f"Connection error encountered: { e } " )
5498+ status .num_connection_errors += 1
5499+ _emit_first_error (
5500+ f"Connection error encountered: { e } . This can indicate network instability or bandwidth limitations." ,
5501+ dedup_key = ("connection-error" , str (e )),
5502+ )
5503+ logger .debug (f"Connection error for { ident } : { e } " )
54175504 error_logs [ident ].append (str (e ))
5505+ connection_error_times .append (time .time ())
5506+ connection_errors_since_adjust += 1
5507+ connection_errors_since_last_status += 1
5508+ maybe_adjust_for_connection_errors ()
54185509 if attempts_left - 1 > 0 and not stop_event .is_set ():
54195510 backoff = random .uniform (1 , 2 ) * (2 ** (max_retries - attempts_left ))
54205511 await _maybe_retry (backoff )
@@ -5516,7 +5607,7 @@ async def status_reporter() -> None:
55165607 if status_report_interval is None :
55175608 return
55185609 try :
5519- nonlocal timeout_errors_since_last_status
5610+ nonlocal timeout_errors_since_last_status , connection_errors_since_last_status
55205611 while not stop_event .is_set ():
55215612 await asyncio .sleep (status_report_interval )
55225613 if stop_event .is_set () or processed >= status .num_tasks_started :
@@ -5525,6 +5616,7 @@ async def status_reporter() -> None:
55255616 "Periodic status update" , force = True , label = None
55265617 )
55275618 timeout_errors_since_last_status = 0
5619+ connection_errors_since_last_status = 0
55285620 except asyncio .CancelledError :
55295621 pass
55305622
@@ -5646,6 +5738,7 @@ async def status_reporter() -> None:
56465738 quiet = quiet ,
56475739 global_cooldown = global_cooldown ,
56485740 rate_limit_window = rate_limit_window ,
5741+ connection_error_window = connection_error_window ,
56495742 token_sample_size = token_sample_size ,
56505743 status_report_interval = status_report_interval ,
56515744 planning_rate_limit_buffer = planning_rate_limit_buffer ,
@@ -5660,7 +5753,12 @@ async def status_reporter() -> None:
56605753 logger .warning (f"{ status .num_tasks_failed } requests failed." )
56615754 if status .num_rate_limit_errors > 0 :
56625755 logger .warning (
5663- f"{ status .num_rate_limit_errors } rate limit errors encountered; consider reducing concurrency."
5756+ f"{ status .num_rate_limit_errors } rate limit errors encountered; consider reducing concurrency via lower n_parallels."
5757+ )
5758+ if status .num_connection_errors > 0 :
5759+ logger .warning (
5760+ f"{ status .num_connection_errors } API connection errors encountered, indicating network instability or bandwidth limitations; "
5761+ "consider reducing concurrency via lower n_parallels."
56645762 )
56655763 if status .num_timeout_errors > 0 :
56665764 logger .warning (f"{ status .num_timeout_errors } timeouts encountered." )
0 commit comments