This commit is contained in:
team3
2026-07-05 20:43:25 +02:00
parent 250ea0b764
commit ed5cf146c3
8 changed files with 10887 additions and 4 deletions

View File

@@ -77,6 +77,15 @@ def _claude_error(label: str, returncode: int, stdout: str, stderr: str) -> str:
return f"{label} (exit {returncode}, no output)"
_INFRA_MARKERS = ("HTTP 429", "HTTP 5", "rate_limit", "Timeout after",
"ConnectError", "ConnectTimeout", "ReadError", "RemoteProtocolError")
def _is_infra(err: str) -> bool:
"""Transport-/Infra-Fehler (retry + pause) statt inhaltlichem Fehlschlag."""
return any(m in (err or "") for m in _INFRA_MARKERS)
def _gather_error(label: str, results: list) -> str:
for r in results:
if isinstance(r, BaseException):
@@ -158,7 +167,8 @@ def _enum_map_schema(key: str, allowed):
_yesno_schema = _enum_map_schema("relevant", _YESNO) # triage gate ∈ ja/nein
from config import MAX_RESTARTS as _MAX_RESTARTS, HEDGE_NACH_S as _HEDGE_NACH_S # noqa: E402 — zentral tunebar
from config import (MAX_RESTARTS as _MAX_RESTARTS, HEDGE_NACH_S as _HEDGE_NACH_S, # noqa: E402 — zentral tunebar
INFRA_MAX_RETRIES as _INFRA_MAX_RETRIES, INFRA_BACKOFF_BASE as _INFRA_BACKOFF)
async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout: int, provider: str, cancelled=None, *, grace: int | None = None) -> list | None:
@@ -176,6 +186,8 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
keeps running until it stands. Returns: `quorum` to `len(slots)` results.
"""
attempts = {i: 0 for i in range(len(slots))}
infra_attempts = {i: 0 for i in range(len(slots))}
infra_erschoepft: set[int] = set()
tasks: dict[asyncio.Task, int] = {}
keys: dict[asyncio.Task, str] = {}
born: dict[asyncio.Task, float] = {}
@@ -267,17 +279,32 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
return results
continue
_log(topic, f"{label} {i + 1} (attempt {attempts[i] + 1}): {err}")
infra = _is_infra(err)
_log(topic, f"{label} {i + 1} (attempt {attempts[i] + 1}{' infra' if infra else ''}): {err}")
attempts[i] += 1
# If the minimum already stands, restarts are pointless — the restart
# would be killed at the grace end anyway. A still-running twin IS the retry.
enough = grace is not None and len(results) >= quorum
zwilling = any(i2 == i for i2 in tasks.values())
if attempts[i] <= _MAX_RESTARTS and not enough and not zwilling and not (cancelled and cancelled()):
if enough or zwilling or (cancelled and cancelled()):
continue
if infra:
# 429/Timeout/Netz: eigener Zähler + wachsende Pause, dann Eskalation
infra_attempts[i] += 1
if infra_attempts[i] > _INFRA_MAX_RETRIES:
infra_erschoepft.add(i) # kein Respawn → raise am Quorum-Miss
continue
await asyncio.sleep(_INFRA_BACKOFF * 2 ** (infra_attempts[i] - 1))
spawn(i)
elif attempts[i] <= _MAX_RESTARTS:
spawn(i)
if len(results) >= quorum: # all slots done, minimum stands (only reachable with grace)
return results
_log(topic, f"{label}: quorum {quorum} not reached ({len(results)} valid)")
if infra_erschoepft:
raise AgentInfraError(
f"{label}: {len(infra_erschoepft)} Slot(s) nach {_INFRA_MAX_RETRIES} "
"Infra-Retries erschöpft (429/Timeout)")
return None
finally:
for task, i in tasks.items():
@@ -300,6 +327,11 @@ class GenContext:
OK, CANCELLED, FAILED = "ok", "cancelled", "failed"
class AgentInfraError(Exception):
"""Slot nach Infra-Retries erschöpft (429/Timeout). KEIN inhaltliches Urteil —
der Aufrufer pausiert den Lauf, nie fail-open."""
async def run_single_slot(
ctx: GenContext, label: str, *,
key: str, prompt: str, role: str, capabilities: str, payload, timeout: int, on_line=None,