The lease, stall and probe-TTL guards only ran inside GatewaySettings.from_env,
so from_settings() and direct construction could produce settings that violate
the class's own invariants: the permit lease could expire mid-request (silently
exceeding the concurrency quota), a normal slow first token could be killed as a
stall, and a half-open probe could be taken over while still in flight.
Guards move into __post_init__ as _validate_* methods, matching every frozen
dataclass in types.py, so all six factories plus dataclasses.replace are covered
by one check. Adds a non-empty sources invariant that previously only from_env
enforced. Messages now name fields instead of env keys, since callers who build
settings by hand never set those keys.
Round 5 left three residual failure classes; ladder exhaustion (3.2%
of structured calls with a single re-ask) and 429 leakage (5.8%, AIMD
oscillating above the sustainable point) are addressable: re-ask
default goes 1 to 2 (conservative vs instructor's 3) and the AIMD cut
factor drops to 0.5.
Threshold auto-raise now uses per-source concurrency only (the M2
global-concurrency formula neutered the breaker at scale). Four new
optional keys: MIN_CALLS, FAIL_RATE, WINDOW_S, MAX_COOLDOWN_S.
Includes config aggregation for multi-source env keys, from_env and
from_settings factories with explicit shared-backend injection,
gather_bounded, top-level exports, tightened import-linter layers with
the gate removed from the Makefile, and the finalized .env.example.