Merge pull request 'reopen: act on drb-correlation-review of e972cac' (#177) from fix/reopen-review into main
Build & Deploy / Build & push images (push) Successful in 4m26s
Build & Deploy / Deploy Firestore rules & indexes (push) Failing after 3s
Build & Deploy / Deploy to VM (push) Successful in 1m45s
Build & Deploy / Report a failed deploy (push) Successful in 1s

This commit was merged in pull request #177.
This commit is contained in:
2026-09-26 19:43:38 -04:00
2 changed files with 32 additions and 7 deletions
@@ -224,6 +224,16 @@ def _normalize_unit(unit: str) -> str:
return key or unit.strip().lower()
def _after_close(inc: dict, now: datetime) -> bool:
try:
closed = datetime.fromisoformat(str(inc.get("resolved_at") or "").replace("Z", "+00:00"))
except ValueError:
return True
if closed.tzinfo is None:
closed = closed.replace(tzinfo=timezone.utc)
return now > closed
def _is_trackable_unit(unit: str) -> bool:
"""
Whether a unit is concrete enough to hold an incident open until it clears.
@@ -2029,7 +2039,8 @@ async def _update_incident(
incident_id = inc["incident_id"]
call_ids = list(inc.get("call_ids") or [])
if call_id not in call_ids:
is_new_call = call_id not in call_ids
if is_new_call:
call_ids.append(call_id)
talkgroup_ids = list(inc.get("talkgroup_ids") or [])
@@ -2101,10 +2112,11 @@ async def _update_incident(
# thin traffic rides along without extending its life.
if refresh_activity:
updates["updated_at"] = _floor_at_started_at(inc, now).isoformat()
updates["substantive_call_count"] = (
inc.get("substantive_call_count")
if inc.get("substantive_call_count") is not None else len(inc.get("call_ids") or [])
) + 1
if is_new_call: # a second scene of the same call is not a second call
updates["substantive_call_count"] = (
inc.get("substantive_call_count")
if inc.get("substantive_call_count") is not None else len(inc.get("call_ids") or [])
) + 1
else:
updates["last_thin_at"] = now.isoformat()
# Update incident type when a re-classified call provides a concrete type.
@@ -2122,8 +2134,11 @@ async def _update_incident(
# Signal-based auto-resolve: every tracked unit has cleared, none still active.
# Requires at least one unit to have explicitly signalled back-in-service so we
# don't fire on incidents where units were never tracked (no unit mentions at all).
if inc.get("status") == "resolved":
# A timer close was provisional and a related call just arrived.
if inc.get("status") == "resolved" and refresh_activity and _after_close(inc, now):
# A timer close was provisional and a related, substantive call arrived
# after it. A thin "10-4" rides along without reopening (it would not
# refresh updated_at, so the next sweep would just close it again),
# and neither does a sweep link of a call from before the close.
updates.update({"status": "active", "resolved_at": None, "resolved_via": None,
"reopenable": False, "reopened_count": (inc.get("reopened_count") or 0) + 1})
logger.info(f"Correlator: reopened timer-closed incident {incident_id} (call {call_id})")
@@ -2373,6 +2388,10 @@ async def _find_cross_system_parent(
best_score = 0.0
for inc in recent:
# A timer-closed incident is in `recent` only so a related call can
# reopen it; it must not be adopted as another agency's parent.
if inc.get("status") != "active":
continue
# Only cross-system candidates
if system_id in (inc.get("system_ids") or []):
continue
@@ -93,3 +93,9 @@ def test_thin_calls_do_not_fill_the_call_cap():
assert ic._incident_at_capacity(inc, now) is None
legacy = {k: v for k, v in inc.items() if k != "substantive_call_count"}
assert ic._incident_at_capacity(legacy, now).startswith("call_cap")
def test_reopen_only_for_a_call_after_the_close():
closed = {"resolved_at": "2026-09-22T15:00:00+00:00"}
assert ic._after_close(closed, datetime(2026, 9, 22, 15, 5, tzinfo=timezone.utc))
assert not ic._after_close(closed, datetime(2026, 9, 22, 14, 55, tzinfo=timezone.utc))