Track live input health and recover GPS failover - #106
Closed
FrogAi wants to merge 2 commits into
Closed
Conversation
This was referenced Aug 9, 2026
FrogAi
force-pushed
the
codex/track-input-health-and-recover-gps
branch
from
August 9, 2026 16:50
669b5e0 to
8dcfd0a
Compare
Contributor
Author
|
Closing this version because it encodes several undocumented product policies—required input health, GPS fix/failover, stale-output clearing, and recovery—inside one six-file state machine. Those contracts need to be established before implementation, then any proven defect should be split to the smallest owner. Keeping this open would ask the maintainer to review policy guesses rather than a bounded correction. |
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.
Summary
Read()API.carStatehealth gates the envelope, GPS/map/route health gates only map-derived output, and vision-curve output survives both.Motivation
At Base
68813e05, a subscriber reports success after structural decode and union selection. It does not inspectEvent.validor retain when the message was received, so invalid or indefinitely retained car, model, and GPS data is indistinguishable from live input.GPS selection compounds that problem. The first decoded external packet permanently latches the external subscriber; the internal queue is no longer drained, so a later external stall cannot fall back to a healthy internal source. The main loop also republishes retained state as valid before reading new inputs, which allows stale limits, hazards, curves, and paths to remain externally valid.
These are one transition contract rather than independent fixes: a normal 20 Hz poll can legitimately see no new packet between 10 Hz external GPS updates, so failover needs the same persisted validity and receiver-observed freshness state used to determine output health.
Health contract
carStateEvent.validand local receipt age < 500 msmapdOut. Every published field derives from car state, so this is the one input that invalidates the whole envelope. On recovery, skip the unknown outage interval instead of feeding it into the car timing average (gap threshold stays 100 ms).modelV2Event.validand local receipt age < 500 msEvent.valid, a reported fix, and local receipt age < 1 sEvent.valid, a reported fix, and local receipt age < 10 stileLoadedcontinues to report the condition.mapdIn/mapdCliEvent.valid; these on-demand command envelopes do not expire.Freshness uses receiver-local time, not producer
logMonoTimeor GPS wall time. The boundary is strict: a sample is stale at exactly its timeout.The
carStatewindow is 500 ms rather than a ten-period 100 ms, because the daemon's own loop can exceed 100 ms between reads when a tile-boundary crossing loads and parses a tile; a tighter window converts that routine work into spurious invalid frames. The 100 ms threshold is retained where it belongs, as the inter-message gap that rebases the car timing average.GPS fix compatibility accepts
hasFixor the legacy bit-zero fix flag. Cap'n Proto exposes no presence bit forhasFix, so an odd legacy flag remains authoritative whenhasFixis false.Source selection is debounced: external is adopted immediately when no healthy internal fix exists, but while internal is healthy external must stay healthy for 2 s before it takes over. Failover away from a stale external source is never delayed.
Behavior
tileLoadedfalsemapdIncommandValidation
669b5e07changes six production files and adds no test files.bedfde92drove the corrections in this head. It reproduced, with a fake-clock harness over real msgq queues, 18 GPS source switches in 10 s when external GPS degraded to roughly a 1 s cadence while internal stayed healthy, and established that an all-or-nothing output gate silenced vision-curve control and external speed limits whenever GPS, tiles, or current-road resolution were unavailable — including permanently on an install with no tiles downloaded. That audit separately verified the failover state machine itself against its specification: cold start, preference, strict timeout boundaries, recovery, invalid-event and no-fix rejection, and legacy flag acceptance.bedfde92), adopts external immediately when no internal fix exists, defers promotion while internal is healthy, fails over on external staleness, and reports both-dead as no source withFresh()false.make buildsuccessfully for exact Head669b5e07.Compatibility and scope
Read()signature changes.Event.validonmapdOutnow carries health semantics: it is false when car state is stale, and its payload is default-zero in that case. Consumers that ignoreEvent.validsee zeroed fields during a car-state outage.tileLoadedcontinues to signal the missing-map case.Event.valid=falsedownload/cancel inputs.mapdExtendedOutremains valid for independent settings and download progress; only its GPS/map/current-road path is emptied when unavailable.Merge order with #114
#114 rate-limits tile-load retries in the same loop this PR restructures. The two conflict in
main.go, but only in the both-added declaration block near the top, where the union is the only resolution that compiles. The semantically important hunk auto-merges silently about 115 lines below, inside the GPS branch, where a maintainer reviewing the conflict would never look — the same class of hazard that made the #112/#113 merge silently corrupt tiles.A trial merge confirms #114's retry bookkeeping lands above this PR's early exits, so the backoff is not defeated. What remains is two stacked "map unusable" exits with different bookkeeping: this PR's
if !state.MapValid { ClearRoute(); continue }and #114's bare tile-loaded gate, which sets neitherMapValidnor clears the route. No currently reachable stale-state leak was constructed, so it is redundant rather than broken — but becauseMapValidwould then be assigned only inside the backoff branch, any future widening of the retry delay makes the bare gate the live exit and silently skips this PR's invalidation.Land this PR first, then fold #114's gate into the single form
state.MapValid = state.Data.Loaded; if !state.MapValid { state.ClearRoute(); continue }rather than leaving both. Composition with #118 and #119 is clean and needs no intervention; the four-PR stack is byte-identical to the union of the pairs.Rebased onto current
main(8dcfd0a)Two things landed in
mainunderneath this PR: shadow-subscription settings, which changed how the GPS subscribers are constructed, and #114's tile-retry rate limiting, which rewrote the same map-load block this PR restructures. The rebase keepsmain'sSubscriberSettings.ShadowGpsLocation/ShadowGpsLocationExternalwiring alongside this PR's failover state machine, and unions the two declaration blocks.The map-load composition was resolved deliberately rather than mechanically. #114's retry gate is preserved, and
state.MapValid = state.Data.Loadedis now assigned on every GPS tick rather than only inside the retry branch, with an unloaded tile clearing route state before continuing. Leaving both gates in place would have worked today but madeMapValidsticky: it would only be reassigned when the backoff allowed a load attempt, so any future widening of the retry delay would silently skip this PR's invalidation. The collapsed form also means a corrupt tile clears the current way rather than freezing the previous cell's speed limit — the regression #114 was itself corrected for.Re-verified against the rebased base: the failover harness still shows no source flapping across 200 ticks of degraded external cadence, immediate adoption of external when no internal fix exists, deferred promotion while internal is healthy, failover on external staleness, and both-dead reported as no source with
Fresh()false. FrogAi Build #59 passed for the exact head.