Suppress false all-clears when a hazard is superseded (#145) #149
No reviewers
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot!149
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/false-all-clear-supersession"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #145.
Problem
NWS routinely replaces one product with another for the same hazard — an Extreme Heat Watch (
XH.A) becomes a Heat Advisory (HT.Y), a watch becomes a warning. The superseded product simply drops out of the active feed, and_clear_missing_alertsread that as "the hazard is over" and announced an all-clear.Confirmed in production:
NWS_KLSX_XH_A_0002had VTEC/O.NEW.KLSX.XH.A.0002.260726T1600Z-260729T0000Z/and storedexpiresof 2026-07-29, and was cleared with a delivered all-clear on 2026-07-24 17:16 — 4.5 days early. On the Rolla location the replacing Heat Advisory was inserted at16:28:03.81and the watch cleared at16:28:03.96, 150 ms apart in the same poll cycle. Same pattern in June (XH_A_0001, valid to 07-03, cleared 06-27).Why the obvious fix is not enough
Checking "is another alert still active here" only works if the replacement was recorded. It often is not. The replacement is frequently less severe than what it replaces, so
_meets_thresholddrops it before any row exists. The four affected locations runmin_severity = watch; the replacing advisory left no trace whatsoever, so from the database the hazard looked finished.The guard therefore has to consult the feed at the point of geographic match, before severity filtering.
Change
Guard A — hazard-family presence. Track
(location_id, hazard_family)for every alert covering a location geographically, before the severity and exclusion filters. A record whose family is still present was superseded, not lifted: it is still cleared (it really has left the feed) but its all-clear is suppressed.Family comes from the VTEC phenomenon already embedded in
nws_alert_id(NWS_{office}_{phen}_{sig}_{etn}), mapped through a small table. This matters because the phenomenon code changes across the transition —XH→HT— so matching on phenomenon alone would miss exactly the case being fixed. Unmapped phenomena returnNoneand fall through to guard B, so an incomplete table degrades to today's behaviour rather than silently swallowing real all-clears.Guard B — equal-or-greater severity still active. Never announce an all-clear while an alert of equal or greater severity is live at that location. Deliberately placed in
send_pending_lifted_notificationsso it covers every clearing path — the poll sweep, an explicit Cancel, andexpire_alerts_job(#147) — not just the one that produced this report. The motivating case is a Tornado Watch cancelled while a Tornado Warning is live: same code path, and there a false all-clear is a safety failure rather than a confusing message.Suppression stamps
lifted_notified_atat clear time rather than leaving it NULL, so the row is not re-selected on every subsequent cycle — the loop #148 is currently stuck in.Tests
tests/test_all_clear_supersession.py, including two controls so the guards cannot silently over-suppress:XH/HT/EHas heat and returnsNonefor non-VTEC and unmapped phenomenaVerification
ruff check .python -m compileall appalembic upgrade headon fresh Postgres 16Not addressed here
#146 (pagination truncation), #147 (
expire_alerts_jobhas no feed cross-check) and #148 (Webex retry loop) are separate routes to a bad all-clear or a stuck delivery. Guard B partially mitigates #147 but is not a substitute for it.🤖 Generated with Claude Code