correlator: remove is_dispatch and the tactical fit path entirely (#134)

Full removal, not a hardcoded flag: _is_dispatch_channel, _DISPATCH_TG_RE, the is_dispatch parameter, and _call_fits_incident's tactical branch are gone. One evaluation path for every channel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
This commit is contained in:
Logan Cusano
2026-09-13 14:49:25 -04:00
co-authored by Claude Sonnet 5
parent 0473e6a583
commit 014f5e071f
9 changed files with 110 additions and 238 deletions
Submodule .claude/worktrees/agent-a0ef2efe6c3085b35 added at fe643924c7
Submodule .claude/worktrees/agent-a710d266082748653 added at 52edbf105c
Submodule .claude/worktrees/agent-aa99ec6ec04c2495a added at 77f1d2f93f
Submodule .claude/worktrees/agent-ae8ad776f3d4d2f75 added at d60fef67ad
Submodule .claude/worktrees/agent-af608114d71b03314 added at 8a0412b529
+12 -24
View File
@@ -90,31 +90,19 @@ class Settings(BaseSettings):
unit_continuity_max_idle_minutes: int = 20 # unit-continuity path: skip if incident idle > this unit_continuity_max_idle_minutes: int = 20 # unit-continuity path: skip if incident idle > this
recorrelation_scan_minutes: int = 60 # re-examine orphaned calls ended within this window recorrelation_scan_minutes: int = 60 # re-examine orphaned calls ended within this window
tg_fast_path_idle_minutes: int = 90 # fast path: max minutes since incident last updated tg_fast_path_idle_minutes: int = 90 # fast path: max minutes since incident last updated
# Dispatch channels only: tier-2 thin calls attach to a lone candidate idle < this. # Tier-2 thin calls attach to a lone candidate idle < this, on every
# Was 10, which is long enough for the channel to have moved on to something else: # channel (server-26#133/#134 removed the dispatch/tactical split — a
# on 2026-08-16 a "72 at Holland Station" incident absorbed a Grand Central train # channel's name doesn't change how much scrutiny it gets). Was 10, which
# meet 9.6 min later, and a status check absorbed a records lookup at 9.7 min. # is long enough for the channel to have moved on to something else: on
# Across that dump every correct thin attach was <= 3.4 min idle and every wrong # 2026-08-16 a "72 at Holland Station" incident absorbed a Grand Central
# one was >= 8.2, so 5 separates them with room on both sides. Genuine # train meet 9.6 min later, and a status check absorbed a records lookup
# back-and-forth is handled by the 30-second tier-1 path above this. # at 9.7 min. Every correct thin attach in that dump was <= 3.4 min idle
# Second consumer (server-26#115): routers/upload.py's LLM-orphan-gate # and every wrong one was >= 8.2, so 5 separates them with room on both
# escape hatch (_recent_incident_on_same_talkgroup) always uses this same # sides. Genuine back-and-forth is handled by the 30-second tier-1 path
# value now — no dispatch/tactical branch there since #133. Retuning this # above this. Also the escape hatch in routers/upload.py's LLM-orphan gate
# for fast/thin reasons moves that gate's behavior too — check both call # (_recent_incident_on_same_talkgroup, server-26#115) — check both call
# sites before changing it. (tg_thin_idle_minutes below is now unused in # sites before retuning this.
# production — is_dispatch is hardcoded True at its only real call site,
# incident_correlator.py's fast-path — see server-26#134.)
tg_dispatch_thin_idle_minutes: int = 5 tg_dispatch_thin_idle_minutes: int = 5
# Every other channel: tier-2 thin calls attach to a lone candidate idle < this.
# Non-dispatch talkgroups previously had NO tier-2 bound at all — they used the
# whole 90-minute tg_fast_path_idle_minutes window with no single-candidate
# requirement and no fit test, which is the widest version of the 2026-08-20
# over-merge. A tactical channel really is dedicated to one scene, so it earns
# a longer window than a dispatch backbone, but not an unbounded one: 15 sits
# inside the 20-minute tactical-default window in _call_fits_incident, so the
# no-evidence thin path is never more permissive than the fit-tested path on
# the same channel.
tg_thin_idle_minutes: int = 15
# ── Hard caps: an incident past either of these stops accepting calls ────── # ── Hard caps: an incident past either of these stops accepting calls ──────
# Enforced on every correlation path (see _incident_at_capacity). Pairwise fit # Enforced on every correlation path (see _incident_at_capacity). Pairwise fit
+63 -124
View File
@@ -98,13 +98,6 @@ _PURSUIT_PROXIMITY_KM = 20.0 # expanded radius for moving incidents
# overlap with the candidate OR a distance under this tighter bar. # overlap with the candidate OR a distance under this tighter bar.
_LOCATION_TIGHT_PROXIMITY_KM = 0.2 _LOCATION_TIGHT_PROXIMITY_KM = 0.2
_DISPATCH_TG_RE = re.compile(
r"\bdispatch\b|\bdisp\b"
r"|\bpatched\b" # patched channels aggregate multiple call streams
r"|\bprimary\b", # "Primary" channels serve as shared backbones
re.IGNORECASE,
)
# Matches route/road identifiers in location strings for cross-system parent detection. # Matches route/road identifiers in location strings for cross-system parent detection.
# Groups: numbered routes (Route 202, NY-9, US-6, I-87, CR-35) and named parkways/highways. # Groups: numbered routes (Route 202, NY-9, US-6, I-87, CR-35) and named parkways/highways.
_ROAD_RE = re.compile( _ROAD_RE = re.compile(
@@ -494,13 +487,6 @@ def _resolve_incident_title(
return {} return {}
def _is_dispatch_channel(talkgroup_name: Optional[str]) -> bool:
"""True when the talkgroup is a shared dispatch backbone (not a tactical/working channel)."""
if not talkgroup_name:
return False
return bool(_DISPATCH_TG_RE.search(talkgroup_name))
def _incident_idle_minutes(inc: dict, now: datetime) -> float: def _incident_idle_minutes(inc: dict, now: datetime) -> float:
"""Minutes since the incident was last updated (or started). Returns 9999 on parse error.""" """Minutes since the incident was last updated (or started). Returns 9999 on parse error."""
try: try:
@@ -976,15 +962,10 @@ def _run_decision(ctx: dict) -> dict:
# directly on the Firestore call doc). Fall back to the call doc so that # directly on the Firestore call doc). Fall back to the call doc so that
# dispatch-channel strictness works regardless of how the call arrived. # dispatch-channel strictness works regardless of how the call arrived.
effective_talkgroup_name = talkgroup_name or call_doc.get("talkgroup_name") effective_talkgroup_name = talkgroup_name or call_doc.get("talkgroup_name")
# server-26#134: always dispatch-strict now, not name-guessed — a
# channel labeled "tac"/"tactical" is rare and no less scrutinized in
# practice than any other. _call_fits_incident's tactical branch is
# kept, unreached, in case that's ever wrong.
is_dispatch = True
if effective_talkgroup_name != talkgroup_name: if effective_talkgroup_name != talkgroup_name:
logger.info( logger.info(
f"Correlator: talkgroup_name missing from request for call {call_id}, " f"Correlator: talkgroup_name missing from request for call {call_id}, "
f"resolved from call doc: {effective_talkgroup_name!r} → is_dispatch={is_dispatch}" f"resolved from call doc: {effective_talkgroup_name!r}"
) )
tg_matches = [ tg_matches = [
@@ -1028,10 +1009,7 @@ def _run_decision(ctx: dict) -> dict:
# single-candidate requirement and no fit test of any kind. Four # single-candidate requirement and no fit test of any kind. Four
# hours is not a bound, and neither is ninety minutes. # hours is not a bound, and neither is ninety minutes.
THIN_CONVERSATIONAL_SECS = 30 THIN_CONVERSATIONAL_SECS = 30
thin_window_min = ( thin_window_min = settings.tg_dispatch_thin_idle_minutes
settings.tg_dispatch_thin_idle_minutes if is_dispatch
else settings.tg_thin_idle_minutes
)
very_recent = [ very_recent = [
inc for inc in tg_recent inc for inc in tg_recent
if _idle_gate_minutes(inc, now) * 60 <= THIN_CONVERSATIONAL_SECS if _idle_gate_minutes(inc, now) * 60 <= THIN_CONVERSATIONAL_SECS
@@ -1053,8 +1031,7 @@ def _run_decision(ctx: dict) -> dict:
if len(thin_pool) > 1: if len(thin_pool) > 1:
logger.info( logger.info(
f"Correlator fast-path thin (tier-2): {len(thin_pool)} active incidents " f"Correlator fast-path thin (tier-2): {len(thin_pool)} active incidents "
f"on {'dispatch' if is_dispatch else 'tactical'} channel — " f"— ambiguous, skipping thin call {call_id}"
f"ambiguous, skipping thin call {call_id}"
) )
thin_pool = [] thin_pool = []
@@ -1084,14 +1061,14 @@ def _run_decision(ctx: dict) -> dict:
candidate = tg_recent[0] candidate = tg_recent[0]
logger.info( logger.info(
f"Correlator fast/single: call {call_id} vs incident {candidate['incident_id']} " f"Correlator fast/single: call {call_id} vs incident {candidate['incident_id']} "
f"tg_name={effective_talkgroup_name!r} is_dispatch={is_dispatch} " f"tg_name={effective_talkgroup_name!r} "
f"idle={round(_incident_idle_minutes(candidate, now), 1)}min " f"idle={round(_incident_idle_minutes(candidate, now), 1)}min "
f"call_units={call_units} inc_units={candidate.get('units')} " f"call_units={call_units} inc_units={candidate.get('units')} "
f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}" f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}"
) )
fit, fit_signal = _call_fits_incident( fit, fit_signal = _call_fits_incident(
candidate, call_units, call_vehicles, coords, candidate, call_units, call_vehicles, coords,
settings.location_proximity_km, is_dispatch=is_dispatch, settings.location_proximity_km,
call_embedding=call_embedding, now=now, call_embedding=call_embedding, now=now,
reassignment=reassignment, reassignment=reassignment,
) )
@@ -1101,13 +1078,12 @@ def _run_decision(ctx: dict) -> dict:
"corr_path": "fast/single", "corr_path": "fast/single",
"corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1), "corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1),
"corr_fit_signal": fit_signal, "corr_fit_signal": fit_signal,
"corr_is_dispatch": is_dispatch,
} }
if fit_signal == "unit_overlap" and call_units: if fit_signal == "unit_overlap" and call_units:
corr_debug["corr_matched_units"] = _matching_units(call_units, candidate.get("units")) corr_debug["corr_matched_units"] = _matching_units(call_units, candidate.get("units"))
logger.info( logger.info(
f"Correlator fast-path: call {call_id} → {candidate['incident_id']} " f"Correlator fast-path: call {call_id} → {candidate['incident_id']} "
f"(signal={fit_signal}, is_dispatch={is_dispatch})" f"(signal={fit_signal})"
) )
else: else:
logger.info( logger.info(
@@ -1123,14 +1099,14 @@ def _run_decision(ctx: dict) -> dict:
# dispatch channel should create its own incident, not be force-merged. # dispatch channel should create its own incident, not be force-merged.
logger.info( logger.info(
f"Correlator fast/disambig: call {call_id} vs incident {candidate['incident_id']} " f"Correlator fast/disambig: call {call_id} vs incident {candidate['incident_id']} "
f"tg_name={effective_talkgroup_name!r} is_dispatch={is_dispatch} " f"tg_name={effective_talkgroup_name!r} "
f"idle={round(_incident_idle_minutes(candidate, now), 1)}min " f"idle={round(_incident_idle_minutes(candidate, now), 1)}min "
f"call_units={call_units} inc_units={candidate.get('units')} " f"call_units={call_units} inc_units={candidate.get('units')} "
f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}" f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}"
) )
fit, fit_signal = _call_fits_incident( fit, fit_signal = _call_fits_incident(
candidate, call_units, call_vehicles, coords, candidate, call_units, call_vehicles, coords,
settings.location_proximity_km, is_dispatch=is_dispatch, settings.location_proximity_km,
call_embedding=call_embedding, now=now, call_embedding=call_embedding, now=now,
reassignment=reassignment, reassignment=reassignment,
) )
@@ -1141,7 +1117,6 @@ def _run_decision(ctx: dict) -> dict:
"corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1), "corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1),
"corr_candidates": len(tg_recent), "corr_candidates": len(tg_recent),
"corr_fit_signal": fit_signal, "corr_fit_signal": fit_signal,
"corr_is_dispatch": is_dispatch,
} }
if fit_signal == "unit_overlap" and call_units: if fit_signal == "unit_overlap" and call_units:
corr_debug["corr_matched_units"] = _matching_units(call_units, candidate.get("units")) corr_debug["corr_matched_units"] = _matching_units(call_units, candidate.get("units"))
@@ -1720,7 +1695,6 @@ def _call_fits_incident(
call_vehicles: list[str], call_vehicles: list[str],
call_coords: Optional[dict], call_coords: Optional[dict],
proximity_km: float, proximity_km: float,
is_dispatch: bool = False,
call_embedding: Optional[list] = None, call_embedding: Optional[list] = None,
now: Optional[datetime] = None, now: Optional[datetime] = None,
reassignment: bool = False, reassignment: bool = False,
@@ -1730,48 +1704,24 @@ def _call_fits_incident(
the incident; signal names the specific evidence that drove the decision. the incident; signal names the specific evidence that drove the decision.
fits=True signals: "unit_overlap" | "vehicle_overlap" | "location_proximity" fits=True signals: "unit_overlap" | "vehicle_overlap" | "location_proximity"
| "time_fallback" | "tactical_default"
fits=False signals: "unit_loc_conflict" | "content_divergence" fits=False signals: "unit_loc_conflict" | "content_divergence"
| "location_conflict" | "no_signal" | "tactical_idle" | "location_conflict" | "no_signal"
Original docstring (logic unchanged): Evaluation order:
Return True if this call plausibly belongs to the given incident. 1. Unit overlap. Same officer = same call. Also runs a location-conflict
guard: geocoded on both sides and clearly different → the officer has
Evaluation order for dispatch channels (is_dispatch=True): moved to a new scene, false positive. No geocode on the call (or on
───────────────────────────────────────────────────────── the incident, with a call geocode) AND the incident is old (≥ 15 min)
1. Unit overlap → content divergence as a location proxy (embedding similarity < 0.82
Same officer = same call. On dispatch channels, also run a location → different scene). Skipped for recent incidents (< 15 min) — an
conflict guard: if both sides carry geocoded coords and they differ update without re-stating the address is normal.
significantly, the officer has moved to a new scene and the unit match 2. Vehicle overlap → True.
is a false positive. 3. Location proximity. Both geocoded and close → True; far apart with no
When the call has NO geocoded coordinates AND the incident is old other positive signal → False.
(≥ 15 min), use content divergence as a location proxy: an officer at 4. No positive signal at all → False. A shared channel must not absorb
a genuinely different scene will be talking about clearly different calls by default (server-26#134 — this used to default True within
things. For recent incidents (< 15 min) we skip this proxy — the 20 min on any channel not name-matched as "dispatch"; a talkgroup
officer may simply be giving an update without mentioning the address. named tac/tactical is no less scrutinized in practice than any other).
2. Vehicle overlap → True
3. Location proximity
Both sides geocoded and close → True.
Both sides geocoded and far apart (no other positive signal) → False.
4. No positive signals fired → dispatch fallback
a. Conversational continuity: idle < 2 min → True.
A call arriving within 2 minutes of the last incident activity almost
certainly belongs to the same dispatch thread. "Baker, head over
there too" or "copy that" carries no incident-specific vocabulary but
is unambiguously a response to what was just said. We do not require
embedding similarity here — embeddings capture word meaning, not
conversational context, and short operational messages will always
have lower similarity than the incident's accumulated content.
b. Older incident, no positive signals → False.
A shared dispatch channel must not absorb calls by default.
Tactical / working channel (is_dispatch=False):
────────────────────────────────────────────────
Channel is dedicated to one scene. No evidence of separation ≈ same call.
Default → True.
Thin calls (no units/vehicles/coords) never reach this function — Thin calls (no units/vehicles/coords) never reach this function —
they are intercepted before it in correlate_call. they are intercepted before it in correlate_call.
@@ -1791,44 +1741,43 @@ def _call_fits_incident(
inc_units = _unit_keys(inc.get("units")) inc_units = _unit_keys(inc.get("units"))
matched_units = _matching_units(call_units, inc.get("units")) matched_units = _matching_units(call_units, inc.get("units"))
if matched_units: if matched_units:
if is_dispatch: if call_coords:
if call_coords: # Hard location conflict: geocoded on both sides and clearly different.
# Hard location conflict: geocoded on both sides and clearly different. inc_coords_u = inc.get("location_coords")
inc_coords_u = inc.get("location_coords") if inc_coords_u:
if inc_coords_u: dist_km = _haversine_km(
dist_km = _haversine_km( call_coords["lat"], call_coords["lng"],
call_coords["lat"], call_coords["lng"], inc_coords_u["lat"], inc_coords_u["lng"],
inc_coords_u["lat"], inc_coords_u["lng"], )
) if dist_km > proximity_km:
if dist_km > proximity_km: logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but location_conflict dist={dist_km:.2f}km → unit_loc_conflict")
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but location_conflict dist={dist_km:.2f}km → unit_loc_conflict") return False, "unit_loc_conflict"
return False, "unit_loc_conflict"
elif call_embedding and idle_min >= 15:
# Call has geocode but incident doesn't — fall back to content
# divergence as a location proxy. Without this, stale incidents
# that never geocoded absorb unrelated calls purely on unit
# overlap (e.g. a patrol officer working a second scene 70 min
# after the original call).
inc_emb_u = inc.get("embedding")
if inc_emb_u:
sim = _cosine_similarity(call_embedding, inc_emb_u)
if sim < 0.82:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence (has_call_coords/no_inc_coords) sim={sim:.3f} → content_divergence")
return False, "content_divergence"
elif call_embedding and idle_min >= 15: elif call_embedding and idle_min >= 15:
# No geocode available AND old incident: use content divergence as a # Call has geocode but incident doesn't — fall back to content
# location-proxy veto. After 15+ minutes an officer at a completely # divergence as a location proxy. Without this, stale incidents
# different scene will be discussing clearly different content. # that never geocoded absorb unrelated calls purely on unit
# Skip this for recent incidents — an officer updating on the same # overlap (e.g. a patrol officer working a second scene 70 min
# scene without re-stating the address is normal and their update # after the original call).
# won't share much vocabulary with the original dispatch.
inc_emb_u = inc.get("embedding") inc_emb_u = inc.get("embedding")
if inc_emb_u: if inc_emb_u:
sim = _cosine_similarity(call_embedding, inc_emb_u) sim = _cosine_similarity(call_embedding, inc_emb_u)
if sim < 0.82: if sim < 0.82:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence sim={sim:.3f} → content_divergence") logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence (has_call_coords/no_inc_coords) sim={sim:.3f} → content_divergence")
return False, "content_divergence" return False, "content_divergence"
logger.info(f" fits[{inc_id}]: unit_overlap matched={matched_units} is_dispatch={is_dispatch} → unit_overlap") elif call_embedding and idle_min >= 15:
# No geocode available AND old incident: use content divergence as a
# location-proxy veto. After 15+ minutes an officer at a completely
# different scene will be discussing clearly different content.
# Skip this for recent incidents — an officer updating on the same
# scene without re-stating the address is normal and their update
# won't share much vocabulary with the original dispatch.
inc_emb_u = inc.get("embedding")
if inc_emb_u:
sim = _cosine_similarity(call_embedding, inc_emb_u)
if sim < 0.82:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence sim={sim:.3f} → content_divergence")
return False, "content_divergence"
logger.info(f" fits[{inc_id}]: unit_overlap matched={matched_units} → unit_overlap")
return True, "unit_overlap" return True, "unit_overlap"
# ── 2. Vehicle overlap ──────────────────────────────────────────────────── # ── 2. Vehicle overlap ────────────────────────────────────────────────────
@@ -1882,29 +1831,19 @@ def _call_fits_incident(
return False, "location_conflict" return False, "location_conflict"
# ── 4. No positive signals ──────────────────────────────────────────────── # ── 4. No positive signals ────────────────────────────────────────────────
# Requires at least one positive signal (unit, vehicle, or location match).
# A substantive call with no matching signals is more likely a separate
# incident than a follow-up — two dispatches can arrive within seconds of
# each other on a busy channel. Content-free thin calls are handled before
# this function via the thin path in correlate_call, with a tighter
# 30-second recency window.
logger.info( logger.info(
f" fits[{inc_id}]: no positive signal — is_dispatch={is_dispatch} idle={idle_min:.1f}min " f" fits[{inc_id}]: no positive signal — idle={idle_min:.1f}min "
f"inc_units={list(inc_units)} call_units={call_units} " f"inc_units={list(inc_units)} call_units={call_units} "
f"inc_vehicles={list(inc_vehicles)} call_vehicles={call_vehicles} " f"inc_vehicles={list(inc_vehicles)} call_vehicles={call_vehicles} "
f"call_coords={call_coords is not None} inc_coords={inc_coords is not None}" f"call_coords={call_coords is not None} inc_coords={inc_coords is not None}"
) )
if is_dispatch: return False, "no_signal"
# Dispatch channels require at least one positive signal (unit, vehicle,
# or location match). A substantive call with no matching signals is more
# likely a separate incident than a follow-up to the current one — two
# dispatches can arrive within seconds of each other on a busy channel.
# Content-free thin calls are handled before this function via the thin
# path in correlate_call, with a tighter 30-second recency window.
return False, "no_signal"
# Tactical channel: one scene per channel.
# Within 20 min of the last incident activity, link by default — same
# working channel almost certainly means same scene.
# After 20 min of silence, require at least one positive signal; the same
# frequency can be reused for a new unrelated incident later in the shift.
if idle_min < 20.0:
return True, "tactical_default"
return False, "tactical_idle"
async def _update_incident( async def _update_incident(
+10 -48
View File
@@ -111,55 +111,17 @@ async def upload_call_audio(
def _recent_incident_on_same_talkgroup(ctx: dict) -> bool: def _recent_incident_on_same_talkgroup(ctx: dict) -> bool:
""" """
True when one of the already-loaded recent incidents is running on this True when a recent incident is running on this call's own system +
call's own system + talkgroup AND was active within the last talkgroup, within `settings.tg_dispatch_thin_idle_minutes` (5 min) —
`settings.tg_dispatch_thin_idle_minutes` minutes. Covers the "unit applied uniformly regardless of the talkgroup's name (server-26#134).
dispatched on the dispatch channel, thin acknowledgement 10-30s later" Covers "unit dispatched, thin ack 10-30s later": the ack has no
case: the ack carries no substance of its own but plainly belongs to the substance of its own but plainly belongs to the job just opened.
job just opened.
Always uses `settings.tg_dispatch_thin_idle_minutes` (5 min), regardless Reads ctx["recent"] (the rules engine's own candidate list — no extra
of what the talkgroup is named. An earlier version of this branched on Firestore read). That list is status=="active" incidents only, so an
`_is_dispatch_channel` (mirroring incident_correlator.py's fast/thin idle- already-resolved or capacity-capped same-talkgroup incident won't be
window selection) to use a longer 15-minute window on anything not seen here even if chronologically recent (server-26#115, unresolved —
literally named "dispatch"/"patched"/"primary" — owner correction, would need a dedicated non-status-filtered query).
2026-09-13, from direct scanning experience: a talkgroup named "tac"/
"tactical" genuinely does see materially different traffic only during a
real incident, and that's rare; the overwhelming majority of traffic on
ANY monitored channel — including high-risk stops and pursuits — runs on
the main channel regardless of what it's named. `_is_dispatch_channel`'s
string-match is a naming-convention guess, not a detector of actual
channel behavior, and trusting it here meant a busy single-channel
department not literally named "dispatch" would silently get the more
permissive window and reproduce #115's original bug. One constant,
applied uniformly, is the safer default; a real low-volume channel is the
rare case, not the norm, so erring toward the tighter window costs little.
NOT applied to incident_correlator.py's own fast/thin selection (out of
scope for this pass — a bigger, decision-changing surface, worth its own
review rather than changing under this fix).
This used to be a plain "does any recent incident exist on this
talkgroup" check against a 2-hour window (`correlation_window_hours`).
Measured live in production (server-26#115, CORRELATION_REVIEW_0912.md,
window #3): on a busy dispatch channel producing 3-13 incidents per 2h,
that condition is satisfied almost unconditionally, so the surrounding
LLM-orphan gate never fired on exactly the channels it exists to
protect (0/24 target-shaped calls gated in a 4h window). The docstring's
own intent was always "10-30 seconds", not "hours" — a few minutes is
the right shape.
Reads ctx["recent"] — the same window-filtered candidate list the rules
engine already loaded — so this adds no Firestore read.
Known limitation (server-26#115): ctx["recent"] is derived from
`all_active` in `_build_context` — incidents with `status=="active"`
for the call's org, with over-capacity incidents already dropped by
`_drop_capped` — not a full scan of the `incidents` collection. A
same-talkgroup incident that has already auto-resolved (no longer
"active") or hit `incident_max_calls`/`incident_max_duration_minutes`
will NOT appear here even though it is chronologically recent. A proper
fix needs a dedicated Firestore query that is not status/capacity
filtered — a new read, out of scope for this pass.
Whether this limitation explains the 2/24 unexplained gate misses in the Whether this limitation explains the 2/24 unexplained gate misses in the
window #3 measurement is UNANSWERED, not confirmed either way — a prior window #3 measurement is UNANSWERED, not confirmed either way — a prior
+20 -42
View File
@@ -31,8 +31,10 @@ from app.internal.incident_correlator import (
NOW = datetime(2026, 8, 20, 7, 0, 0, tzinfo=timezone.utc) NOW = datetime(2026, 8, 20, 7, 0, 0, tzinfo=timezone.utc)
# TG 383 from the dump: "Ch 1 (Patched with 155.310)". _DISPATCH_TG_RE matches # TG 383 from the dump: "Ch 1 (Patched with 155.310)", a shared dispatch
# "patched", so this is a shared dispatch backbone carrying the whole department. # backbone carrying the whole department. Kept as two distinct fixture names
# for readability even though the channel's name no longer affects behavior
# (server-26#134).
DISPATCH_TG = "Ch 1 (Patched with 155.310)" DISPATCH_TG = "Ch 1 (Patched with 155.310)"
TACTICAL_TG = "Fireground 2" TACTICAL_TG = "Fireground 2"
@@ -140,12 +142,9 @@ def test_thin_call_with_no_overlap_does_not_attach_on_a_dispatch_channel():
assert _run_decision(_ctx(all_active=[inc], recent=[inc]))["action"] == "orphan" assert _run_decision(_ctx(all_active=[inc], recent=[inc]))["action"] == "orphan"
def test_thin_call_with_no_overlap_does_not_attach_on_a_tactical_channel(): def test_thin_call_with_no_overlap_does_not_attach_on_a_tactical_named_channel():
""" """A channel's name no longer changes anything (server-26#134) — same
The widest version of the bug: non-dispatch talkgroups skipped the tiering assertion as the dispatch-named case above, different fixture name."""
entirely and used the whole 90-minute fast-path window with no
single-candidate requirement, so ANY thin call joined whatever was newest.
"""
inc = _incident(idle_minutes=40) inc = _incident(idle_minutes=40)
decision = _run_decision(_ctx( decision = _run_decision(_ctx(
all_active=[inc], recent=[inc], talkgroup_name=TACTICAL_TG, all_active=[inc], recent=[inc], talkgroup_name=TACTICAL_TG,
@@ -154,10 +153,9 @@ def test_thin_call_with_no_overlap_does_not_attach_on_a_tactical_channel():
def test_tactical_named_channel_uses_the_dispatch_window_now(): def test_tactical_named_channel_uses_the_dispatch_window_now():
"""server-26#134: is_dispatch is hardcoded True — a channel's name no """server-26#134: 14 min was inside the old 15-min tactical window; now
longer changes the thin window. 14 min (the old tactical bound minus 1, every channel uses the 5-min window regardless of name."""
inside the old 15-min window) is now past the 5-min dispatch window.""" inc = _incident(idle_minutes=14)
inc = _incident(idle_minutes=settings.tg_thin_idle_minutes - 1)
decision = _run_decision(_ctx( decision = _run_decision(_ctx(
all_active=[inc], recent=[inc], talkgroup_name=TACTICAL_TG, all_active=[inc], recent=[inc], talkgroup_name=TACTICAL_TG,
)) ))
@@ -234,47 +232,27 @@ def test_back_dated_thin_call_does_not_sail_through_the_recency_gate():
def test_back_dated_call_does_not_bypass_the_content_divergence_veto(monkeypatch): def test_back_dated_call_does_not_bypass_the_content_divergence_veto(monkeypatch):
""" """
Same `9d376ffe` failure mode, but exercised directly against Same `9d376ffe` failure mode, exercised directly against
`_call_fits_incident` on a dispatch channel: unit overlap plus a `_call_fits_incident`: unit overlap plus a back-dated call (incident
back-dated call (incident updated 45 minutes AFTER the call's own updated 45 minutes AFTER the call's own `started_at`, which the sweep
`started_at`, which the sweep passes as `now`) used to make the signed passes as `now`) used to make the signed idle -45, so `idle_min >= 15`
idle -45, so `idle_min >= 15` read False and the content-divergence read False and the content-divergence veto never ran — unit overlap
veto never ran — unit overlap alone forced the merge regardless of alone forced the merge regardless of what the call was actually about.
what the call was actually about. With the gate fixed to compare With the gate fixed to compare distance, idle_min is 45 (>= 15), the
distance, idle_min is 45 (>= 15), the veto runs, and a divergent veto runs, and a divergent embedding (patched below so the assertion
embedding (patched below so the assertion doesn't depend on numpy doesn't depend on numpy being installed in this environment) fails it.
being installed in this environment) fails it.
""" """
monkeypatch.setattr(correlator_mod, "_cosine_similarity", lambda a, b: 0.0) monkeypatch.setattr(correlator_mod, "_cosine_similarity", lambda a, b: 0.0)
inc = _incident(idle_minutes=-45, units=["6-Adam"]) inc = _incident(idle_minutes=-45, units=["6-Adam"])
inc["embedding"] = [1.0, 0.0] inc["embedding"] = [1.0, 0.0]
fits, signal = _call_fits_incident( fits, signal = _call_fits_incident(
inc, call_units=["6-Adam"], call_vehicles=[], call_coords=None, inc, call_units=["6-Adam"], call_vehicles=[], call_coords=None,
proximity_km=settings.location_proximity_km, is_dispatch=True, proximity_km=settings.location_proximity_km,
call_embedding=[0.0, 1.0], now=NOW, call_embedding=[0.0, 1.0], now=NOW,
) )
assert (fits, signal) == (False, "content_divergence") assert (fits, signal) == (False, "content_divergence")
def test_back_dated_call_on_tactical_channel_does_not_get_the_default():
"""
Tactical-channel counterpart: no unit/vehicle/location signal, so the
function falls through to step 4's `idle_min < 20.0` default. A
back-dated call (incident updated 45 minutes after the call's own
started_at) used to read idle_min as -45, which is always < 20.0, so
`tactical_default` fired unconditionally no matter how stale the
incident actually was relative to this call. Fixed, idle_min is the
45-minute distance, which is not < 20.0.
"""
inc = _incident(idle_minutes=-45)
fits, signal = _call_fits_incident(
inc, call_units=[], call_vehicles=[], call_coords=None,
proximity_km=settings.location_proximity_km, is_dispatch=False,
call_embedding=None, now=NOW,
)
assert (fits, signal) == (False, "tactical_idle")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4. Hard caps — path-independent, because pairwise fit tests can't see shape # 4. Hard caps — path-independent, because pairwise fit tests can't see shape
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------