MOR-1191: drive the TX safety supervisor so the watchdog can fire - #2115
Conversation
TxSafetySupervisor.tick() had no caller in src/. It is the sole path to two things: firing the BACKEND_MAX_KEY_DOWN watchdog, and driving _service_release(force=False), the timed retry of a failed OFF. Every other release call site passes force=True. So a rig keyed through the managed path stayed keyed indefinitely — there was no max-key-down bound at all — and a de-key that failed set a retry_due nothing ever came back for. The machinery was correct the whole time. Demonstrated with a controllable clock: an hour past the deadline, zero writes; the instant tick() is called, backend_max_key_down. The ticker lives in ManagedRadioRuntime, which already owns the supervisor and a lifecycle, and is armed lazily by request_on. It is deliberately not a run()/start() hook: an AST scan finds zero ManagedRadioRuntime constructions in src/, so a start hook would be one more thing MOR-1016 could forget, and forgetting it silently restores exactly this defect. Arming inside request_on is complete because _Lease is constructed at exactly one site, inside request_on — confirmed by AST, not grep. The loop retires itself once the lease is gone, so an idle target costs nothing in the strong sense: no task exists, not merely a cheap one. A pending release cannot outlive it — _release is cleared only by the same chained assignment that clears _lease — verified by running the compound case where the watchdog's own OFF fails three times and the same ticker carries it through the 0.25/1.0/2.0 backoff. The interval matches retry_schedule_seconds[0], the finest granularity the supervisor's own retry schedule can express; a longer one would make the first _schedule_retry step unrepresentable. Measured overshoot on a 180 s deadline: 8 ms. Only _lifecycle_lock is taken, and only around the reducer call. _lifecycle_change guards provider identity, which a tick never changes, and waiting on it would park the watchdog behind a retirement — exactly when a keyed rig most needs it. 60 randomized interleavings against a due watchdog: no duplicate WRITE_OFF, no leftover tasks. watchdog_enabled now means configured AND driven. Only a real tick() sets _driven, so the field can no longer advertise a watchdog nothing runs — which is how this defect stayed invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Agent Review: PASS Independent non-author verification at max effort. All three files sha256-pinned before and after; unchanged, and the commit introduced no delta: Every experiment ran in one of three isolated The load-bearing question — can a pending release outlive the ticker? No.The loop retires on AST scan found
Arming, re-arming, shutdown
Shutdown cancels cleanly mid- The author's disclosed non-kill — settled, in the author's favourThe author reported mutation M3 (ticker ignores the The reviewer reproduced the author's hypothesised scenario and confirmed it: with a service that swallows Concurrency60 seeded randomized interleavings of One case was initially flagged — Cost, measured with a real clock
Vacuity and red-before12 mutations, 9 killed. Both mutations the brief specifically demanded are killed — tick called but the transition dropped, and the ticker never retires. So these tests catch a regression, not merely the absence of the feature. Red-before stage 2 verified independently: with base source plus only the ctor knob and the Gates — run by the reviewer, both interpreters
Exactly +4 with an unchanged skip/deselect/xfail breakdown. New test file run 80× across both interpreters, 20 of them under load average 4.1 — 0 failures. Inert for shipped behaviour, provably
On the 8537-vs-8538 anomaly the author flaggedNot reproduced across four full runs, all with an identical breakdown. The reviewer did find genuine order-dependence elsewhere — Non-blocking findings — all filed as MOR-1194, blocking MOR-1016
Hardware boundarySoftware only. No hardware was involved and no hardware claim is made. This does not replace MOR-1033. |
Urgent, blocks MOR-1016. 3 files, 196 net LOC.
The defect
TxSafetySupervisor.tick()had no caller insrc/. It is the sole path to two things:BACKEND_MAX_KEY_DOWNwatchdog —_watchdog_deadlineis read nowhere else;_service_release(force=False), the timed retry of a failed OFF — every other release call site passesforce=True.So a rig keyed through the managed path stayed keyed indefinitely, and a failed de-key set a
retry_duethat nothing ever came back for. The machinery was correct the whole time; nothing drove it.The design
The ticker lives in
ManagedRadioRuntimeand is armed lazily byrequest_on.Deliberately not a
run()/start()hook in theAcquisitionSchedulershape: an AST scan finds zeroManagedRadioRuntimeconstructions insrc/, so a start hook would be one more thing MOR-1016 could forget — and forgetting it silently restores exactly this defect. Arming insiderequest_onis complete because_Leaseis constructed at exactly one site, insiderequest_on, confirmed by AST rather than grep.The loop retires itself once the lease is gone. Measured:
Interval 0.25 s =
retry_schedule_seconds[0], the finest granularity the supervisor's own retry schedule can express; a longer interval would make the first_schedule_retrystep unrepresentable.Locking: only
_lifecycle_lock, and only around the reducer call._lifecycle_changeguards provider identity, which a tick never changes, and waiting on it would park the watchdog behind a retirement — exactly when a keyed rig most needs it. Effects are serviced outside the lock.watchdog_enablednow means configured and driven: only a realtick()sets_driven. The field can no longer advertise a watchdog nothing runs, which is how this defect stayed invisible.Evidence
Independent verification at max effort, all three files sha256-pinned and unchanged. Every experiment ran in an isolated
git archivetree with its own venv, each asserting itsrigplaneresolution before use.The load-bearing question — can a pending release outlive the ticker? The loop retires on
lease_id is None, so if the lease could be cleared while a release was still retrying, acceptance criterion 2 would silently not hold. AST scan found_leasewritten at exactly three sites;_releaseis cleared only by the same chained assignment that clears_lease. Then run, not rested on:['ptt(off)','ptt(off)','read_ptt'], ends IDLE — the retry reaches the wireConcurrency: 60 seeded randomized interleavings of
request_off/release_owner/replace_provider/shutdownagainst a due watchdog at randomized sub-interval offsets — 0 anomalies, 0 leftover tasks. Plus 25 operator-vs-watchdog races with never a duplicateWRITE_OFF.One case was initially flagged —
replace_provideron a due watchdog emitting two OFFs — and the reviewer ran the same scenario in a pure-base tree with no ticker at all and got the identical sequence. Pre-existing MOR-1013 Slice 6 recovery semantics; their assertion was too strict, not the code.12 mutations, 9 killed. Both mutations I specifically demanded are killed: tick called but the transition dropped, and the ticker never retires. So the tests catch a regression, not merely the absence of the feature.
Red-before stage 2 verified independently: with base source plus only the ctor knob (no loop, no arming, no cancel — grep-confirmed), all four tests fail on the defect itself.
pytest tests/ --ignore=tests/integration— baseruff check/format --checklint-importsmypy src/New test file run 80× across both interpreters, 20 of them under load average 4.1 — 0 failures. Not timing-fragile.
Inert for shipped behaviour, provably:
ManagedRadioRuntimehas zero construction sites insrc/;TxSafetySupervisoris constructed in exactly one place, inside it;watchdog_enabledhas zero consumers insrc/andTxSafetySnapshotnever reaches a wire or API payload.The author's disclosed non-kill, and what the reviewer did with it
The author reported mutation M3 — ticker ignores the
_shutdown_pendingfence — as SURVIVED, could not kill it, kept the guard on judgement, and reported 6/7 rather than claiming 7/7.The reviewer reproduced the author's hypothesised scenario and settled it: with a service that swallows
CancelledErrorand the ticker parked inside_service_effects, removing the fence makesshutdown()never return — the loop survives its cancel,lease_idis still non-None, and_complete_shutdown'sgatherhangs. The fence is not redundant. The author's "could not kill it" was a gap in the test, not a property of the code.Follow-up filed — MOR-1194, blocking MOR-1016
The reviewer's findings, none reachable today:
_tick_taskdangles after any external cancel. The loop catchesCancelledErrorand returns without clearing_tick_task, sorequest_onnever re-arms and the watchdog is permanently dead for that runtime — whilewatchdog_enabledstill reportsTrue. Only_complete_shutdowncancels in-tree and it clears first, so this is latent — but it is the exact failure mode this feature exists to prevent, which is why it blocks MOR-1016 rather than sitting in Backlog.sleep(0)passes all four tests, so a busy-spin regression would ship silently.Hardware boundary
Software only. No hardware was run and no hardware claim is made. MOR-1033 remains the FTX-1 physical acceptance gate.
Linear: MOR-1191