SAFETY-119 round 3c: the hidden-bound budget follows the cycle length
checks / unit (pull_request) Successful in 43m29s
infra-pin / fresh (pull_request) Successful in 1s

No single rolling budget met both cases in the sweep (8 false-alarmed on
2 s seed 25 and warned in 1 to 3 of 7 quarters at 15 s). The budget is
now 10 cycles up to a 3.5 s cycle, 8 up to 11 s, 6 up to 16 s, 4 beyond,
per rolling 15 minutes; 8 while the cycle length is unknown. A stop or a
gap over two minutes is not a cycle length. The unsourced "owner
accepted N=8" paragraph is removed from DECISIONS. test_safety119 part 4
now runs 2 s seed 25 and shows a fixed budget of 8 warns there; the
never case runs at 2 s and checks every other quarter at 15 s.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-21 00:57:43 +02:00
co-authored by Claude
parent e27e62ff72
commit 6fd0fa05f1
3 changed files with 129 additions and 76 deletions
+23 -8
View File
@@ -300,10 +300,13 @@ REFUSED_PROGRESS_W = _ctl.PROBE_ANSWER_W / 2
# a healthy battery still settling on a new command is not short.
SHORT_CYCLES, SHORT_SPAN_S = _ctl.SETTLE_CYCLES, REFUSED_SPAN_S
# SAFETY-119: across all hand-backs, the loop's bound is hidden on at most
# this many cycles in any ROLLING_S seconds (a budget measured in
# DECISIONS/SAFETY-119-bound-after-hand-back.md). Without a monotonic clock
# ROLLING_BUDGET cycles in any ROLLING_S seconds, fewer on a slower cycle:
# (cycle seconds up to, cycles), measured in
# DECISIONS/SAFETY-119-bound-after-hand-back.md. Without a monotonic clock
# the budget never refills.
ROLLING_BOUND_CYCLES, ROLLING_S = 8, 900.0
ROLLING_S = 900.0
ROLLING_BUDGET = ((3.5, 10), (11.0, 8), (16.0, 6), (float("inf"), 4))
ROLLING_BOUND_CYCLES = 8 # the budget while the cycle length is unknown
def _refused_interval(sign: int) -> tuple:
@@ -1279,10 +1282,12 @@ class Controller:
self._short_run = 0
# SAFETY-119: did another owner set the wire last cycle; the window
# after a hand-back (monotonic s, the loop's cycles in a row); the
# monotonic s of the bound cycles hidden in the last ROLLING_S.
# monotonic s of the bound cycles hidden in the last ROLLING_S; the
# last cycle's monotonic s and the cycle length.
self._wire_lost = False
self._handback = None
self._hidden = ()
self._settle_t, self._settle_dt = None, None
self.stopping = False
# SAFETY-61. time.monotonic() at the end of the last run_control() pass.
self.loop_pass_mono = None
@@ -5825,6 +5830,7 @@ class Controller:
self._short_run = 0
self._asked_hist = {}
self._wire_lost, self._handback = False, None
self._settle_t = None # a stop is not a cycle length
return
unmet = self.allocation.unmet_w if self.allocation is not None else 0
cut = (resolution.bound_by or "").split("(")[0]
@@ -5867,12 +5873,18 @@ class Controller:
re-seeded from that owner's command and kicked by a meter that does
not show it yet. The settle is SHORT_CYCLES cycles and SHORT_SPAN_S
after the hand-back; losing the wire restarts it. Across all
hand-backs the bound is hidden on at most ROLLING_BOUND_CYCLES cycles
in any ROLLING_S, so a bound that stays is counted however often the
wire changes hands. DECISIONS/SAFETY-119-bound-after-hand-back.md."""
hand-backs the bound is hidden on at most ROLLING_BUDGET[cycle
length] cycles in any ROLLING_S, so a bound that stays is counted
however often the wire changes hands.
DECISIONS/SAFETY-119-bound-after-hand-back.md."""
def aged(t0, span):
return now is None or t0 is None or now - t0 >= span
# The cycle length (a gap over two minutes is not one).
if now is not None and self._settle_t is not None \
and 0.0 < now - self._settle_t <= 120.0:
self._settle_dt = now - self._settle_t
self._settle_t = now
if not owns:
self._wire_lost, self._handback = True, None
return False
@@ -5889,7 +5901,10 @@ class Controller:
if self.loop_clamped[0] and not self.loop_clamped[2]:
kept = tuple(t for t in self._hidden if now is None or (
t is not None and now - t < ROLLING_S))
if len(kept) >= ROLLING_BOUND_CYCLES:
dt = self._settle_dt
budget = ROLLING_BOUND_CYCLES if dt is None else next(
m for up, m in ROLLING_BUDGET if dt <= up)
if len(kept) >= budget:
self._hidden = kept
return False
self._hidden = kept + (now,)