reopen: act on drb-correlation-review of e972cac #177

Merged
logan merged 1 commits from fix/reopen-review into main 2026-09-26 19:43:39 -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() 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: def _is_trackable_unit(unit: str) -> bool:
""" """
Whether a unit is concrete enough to hold an incident open until it clears. 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"] incident_id = inc["incident_id"]
call_ids = list(inc.get("call_ids") or []) 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) call_ids.append(call_id)
talkgroup_ids = list(inc.get("talkgroup_ids") or []) talkgroup_ids = list(inc.get("talkgroup_ids") or [])
@@ -2101,10 +2112,11 @@ async def _update_incident(
# thin traffic rides along without extending its life. # thin traffic rides along without extending its life.
if refresh_activity: if refresh_activity:
updates["updated_at"] = _floor_at_started_at(inc, now).isoformat() updates["updated_at"] = _floor_at_started_at(inc, now).isoformat()
updates["substantive_call_count"] = ( if is_new_call: # a second scene of the same call is not a second call
inc.get("substantive_call_count") updates["substantive_call_count"] = (
if inc.get("substantive_call_count") is not None else len(inc.get("call_ids") or []) inc.get("substantive_call_count")
) + 1 if inc.get("substantive_call_count") is not None else len(inc.get("call_ids") or [])
) + 1
else: else:
updates["last_thin_at"] = now.isoformat() updates["last_thin_at"] = now.isoformat()
# Update incident type when a re-classified call provides a concrete type. # 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. # 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 # 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). # don't fire on incidents where units were never tracked (no unit mentions at all).
if inc.get("status") == "resolved": if inc.get("status") == "resolved" and refresh_activity and _after_close(inc, now):
# A timer close was provisional and a related call just arrived. # 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, updates.update({"status": "active", "resolved_at": None, "resolved_via": None,
"reopenable": False, "reopened_count": (inc.get("reopened_count") or 0) + 1}) "reopenable": False, "reopened_count": (inc.get("reopened_count") or 0) + 1})
logger.info(f"Correlator: reopened timer-closed incident {incident_id} (call {call_id})") 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 best_score = 0.0
for inc in recent: 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 # Only cross-system candidates
if system_id in (inc.get("system_ids") or []): if system_id in (inc.get("system_ids") or []):
continue continue
@@ -93,3 +93,9 @@ def test_thin_calls_do_not_fill_the_call_cap():
assert ic._incident_at_capacity(inc, now) is None assert ic._incident_at_capacity(inc, now) is None
legacy = {k: v for k, v in inc.items() if k != "substantive_call_count"} legacy = {k: v for k, v in inc.items() if k != "substantive_call_count"}
assert ic._incident_at_capacity(legacy, now).startswith("call_cap") 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))