diff --git a/drb-c2-core/app/config.py b/drb-c2-core/app/config.py index 13d251a..d4e0269 100644 --- a/drb-c2-core/app/config.py +++ b/drb-c2-core/app/config.py @@ -51,7 +51,14 @@ class Settings(BaseSettings): 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 tg_fast_path_idle_minutes: int = 90 # fast path: max minutes since incident last updated - tg_dispatch_thin_idle_minutes: int = 10 # dispatch channels only: thin calls only attach to incidents idle < this many minutes + # Dispatch channels only: tier-2 thin calls attach to a lone candidate idle < this. + # Was 10, which is long enough for the channel to have moved on to something else: + # on 2026-08-16 a "72 at Holland Station" incident absorbed a Grand Central train + # meet 9.6 min later, and a status check absorbed a records lookup at 9.7 min. + # Across that dump every correct thin attach was <= 3.4 min idle and every wrong + # one was >= 8.2, so 5 separates them with room on both sides. Genuine + # back-and-forth is handled by the 30-second tier-1 path above this. + tg_dispatch_thin_idle_minutes: int = 5 # Vocabulary learning vocabulary_induction_interval_hours: int = 24 # how often the induction loop runs diff --git a/drb-c2-core/tests/test_correlator_gate.py b/drb-c2-core/tests/test_correlator_gate.py index 2736d51..e001e5d 100644 --- a/drb-c2-core/tests/test_correlator_gate.py +++ b/drb-c2-core/tests/test_correlator_gate.py @@ -146,6 +146,24 @@ def test_thin_call_links_to_the_active_incident_on_its_talkgroup(): assert decision["corr_debug"]["corr_path"] == "fast/thin" +@pytest.mark.parametrize("idle_min", [1.0, 3.4, 4.9]) +def test_thin_call_still_attaches_inside_the_tier2_window(idle_min): + inc = _incident(idle_min) + assert _run_decision(_ctx(all_active=[inc], recent=[inc]))["action"] == "link" + + +@pytest.mark.parametrize("idle_min", [5.1, 8.2, 9.7]) +def test_thin_call_does_not_attach_after_the_channel_has_moved_on(idle_min): + """ + A '10-4' arriving many minutes into silence is new traffic, not a reply. The + old 10-minute window let one incident swallow an unrelated event 9.6 min + later; being the *only* candidate is not evidence, it just means the channel + was quiet, which is when the guess is weakest. + """ + inc = _incident(idle_min) + assert _run_decision(_ctx(all_active=[inc], recent=[inc]))["action"] == "orphan" + + @pytest.mark.asyncio async def test_thin_link_does_not_refresh_updated_at(): with patch("app.internal.incident_correlator.fstore") as mock_fstore: