Repository navigation
fix(supv): a failsafe event must ESCALATE, not re-fire — RTL lasted one tick (#562) - #563
Merged
Merged
Conversation
…ne tick (#562) Every trigger in `FlightSupervisor::step` is LEVEL-triggered, and a vehicle flying home is by definition still outside the fence. So the breach re-sent `Event::Failsafe` every tick and the FSM did as told: (Takeoff | Loiter | Mission, Failsafe) => if have_position { Rtl } else { Land } (Rtl, Failsafe) => Land Tick 0 promoted to RTL; tick 1 re-fired and demoted it. Measured, home 2 m out, fence 0.5 m: tick 0: Takeoff -> Rtl breach=true tick 1: Rtl -> Disarmed breach=true RTL never got a tick to fly. Landing where you are after a fence breach means landing OUTSIDE the fence you were recovering from, which inverts SWREQ-FALCON-SUPV-P01's "geofence -> RTL ACTUATION" and SYSREQ-FALCON-014. The battery failsafe shares that expression and relay-batt's low/critical flags latch (Kani verify_latch_never_clears), so it was equally persistent. THE FIX. `mode_rank` gives each mode a severity, and each send now asks "would this ESCALATE?" instead of "is the condition true?": geofence / low battery / wind -> rank 2 (Rtl) critical battery / motor-out -> rank 3 (Land) send only when want > mode_rank(current) From Takeoff (1) a breach still commands Rtl. In Rtl (2) a continuing breach is no longer re-sent. A CRITICAL pack arriving DURING Rtl still escalates to Land, and motor-out still escalates out of Rtl — which the naive fix (guarding on `!= Mode::Rtl`) would have silently broken, because geofence and battery share the one expression. In Land (3) nothing re-fires, as before. This is relay-fsafe's most-severe-wins + monotone-latch semantics applied at the send. That crate is NOT a drop-in replacement, which is why this is inline: its `Triggers` has no runaway, wind, motor-out or battery-silence field, and `policy_action` cannot reach `Terminate` at all — the action this path uses to cut motors on an attitude runaway. Wiring it is a trigger-set merge plus a re-proof of four exhaustive Kani harnesses, which should not ride on a defect fix. Attitude runaway is untouched: it sends `Terminate`, which is absorbing. WHY NO TEST SAW IT. Five assertions accept the whole recovery family (`Mode::Rtl | Mode::Land | Mode::Disarmed`), which cannot distinguish "RTL flew home" from "RTL lasted 4 ms", and relay-fsm's `verify_failsafe_recovers` is single-step so it cannot see re-invocation by construction. The new test asserts RTL SURVIVES 50 ticks of continuing breach. Its geometry is load-bearing and my first attempt got it wrong: with a 1 cm fence "breached" and "arrived home" (`dist_home < 0.4`) are true simultaneously, RTL ends via the arrival handoff, and the demotion is invisible — I reported a non-reproduction off that. Home 2 m away separates them. Verified to FAIL without the fix ("RTL was demoted 1 ticks in") and pass with it. `cargo test -p falcon-core --all-targets`: 70 passed, 0 failed, incl. the existing wind and battery campaigns and the shadow-stack budget (no new state, only a fn). `cargo kani -p falcon-core` is a no-op: this crate has no cfg(kani) code at all, which is part of what #562 records. NOT FIXED HERE, deliberately: `rtl_latched` is still never cleared, so `NoFailsafeLatched` blocks re-arming permanently. FSAFE-P01 says the latch resets "on disarm"; choosing that reset point is a separate behaviour decision. Refs #562, #558 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
avrabe
enabled auto-merge (squash)
October 14, 2026 05:04
Contributor
✅ Rivet verification gate — falcon174/174 passed
Bench-only artifacts (not run by CI)
Source of truth: |
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the defect reproduced in #562.
Every trigger in
FlightSupervisor::stepis level-triggered, and a vehicle flying home is by definition still outside the fence. So the breach re-sentEvent::Failsafeevery tick, and the FSM did exactly as told:Measured — home 2 m out, fence 0.5 m:
RTL never got a tick to fly. Landing where you are after a fence breach means landing outside the fence you were recovering from — inverting
SWREQ-FALCON-SUPV-P01's "geofence → RTL ACTUATION" andSYSREQ-FALCON-014. The battery failsafe shares that expression andrelay-batt's flags latch (Kaniverify_latch_never_clears), so it was equally persistent.The fix
mode_rankgives each mode a severity, and each send now asks "would this escalate?" rather than "is the condition true?":Rtl)Land)TerminateThat last row is why the naive fix is wrong: guarding on
!= Mode::Rtlwould have silently broken critical-battery escalation, because geofence and battery flow through the same expression at:1935.Why not wire
relay-fsafeinsteadThis is
relay-fsafe's most-severe-wins + monotone-latch semantics applied at the send, and that crate is deliberately not used here: itsTriggershas no runaway, wind, motor-out or battery-silence field, andpolicy_actioncannot reachTerminate— the action this path uses to cut motors. Wiring it is a trigger-set merge plus a re-proof of four exhaustive Kani harnesses, which should not ride on a defect fix. Worth doing as its own piece (ORPHAN-P01).Why no test saw it
Five assertions accept the whole recovery family —
Mode::Rtl | Mode::Land | Mode::Disarmed— which cannot distinguish "RTL flew home" from "RTL lasted 4 ms". Andrelay-fsm'sverify_failsafe_recoversis single-step, so it cannot see re-invocation by construction.The new test asserts RTL survives 50 ticks of continuing breach. Its geometry is load-bearing, and my first attempt got it wrong: with a 1 cm fence, "breached" and "arrived home" (
dist_home < 0.4) are true simultaneously, RTL ends via the arrival handoff, and the demotion is invisible — I reported a non-reproduction off exactly that. Home 2 m away separates them.Verified to fail without the fix (
RTL was demoted 1 ticks in) and pass with it.Checks
cargo test -p falcon-core --all-targets→ 70 passed, 0 failed, including the existing wind and battery campaigns.cargo kani -p falcon-coreis a no-op: this crate has nocfg(kani)code at all, which is itself part of what A sustained geofence breach actuates RTL for ONE 4 ms tick, then demotes it — the breach re-fires Failsafe every tick #562 records.Not fixed here, deliberately
rtl_latchedis still never cleared, soNoFailsafeLatchedblocks re-arming permanently. FSAFE-P01 says the latch resets "on disarm"; choosing that reset point is a separate behaviour decision, not a drive-by.Refs #562, #558
🤖 Generated with Claude Code
https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG