From 7b5258cfdf75dd279b09a673f7eee2b0a5a07238 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 16 Aug 2026 20:56:58 -0400 Subject: [PATCH] Halve the tier-2 thin-call window, from 10 minutes to 5 With over-creation fixed, the incidents that remain are readable enough to judge, and the ones that still do not make sense all fail the same way. A content-free call attaches to the single active incident on its talkgroup if that incident has been idle under tg_dispatch_thin_idle_minutes, and at 10 minutes that is long enough for the channel to have moved on to something else. In the 00:30Z dump a "72 at Holland Station" incident absorbed a Grand Central train-crew meet 9.6 minutes later, and a status check absorbed a records lookup at 9.7. Being the only candidate is not evidence. It means the channel was quiet, which is exactly when guessing is weakest -- the single-candidate rule was meant to avoid picking wrongly among several, not to license a match no other signal supports. Every correct thin attach in that dump was <= 3.4 minutes idle and every wrong one was >= 8.2, so 5 separates them with room on both sides. Real back-and-forth is unaffected: it runs through the 30-second tier-1 path, and the observed conversational replies sit near zero. Tests pin both sides of the new boundary at 4.9 and 5.1 minutes so a later change to this number has to be deliberate. 23 pass. Also corrects a DEFERRED.md entry written earlier today. It claimed nothing ever closes an incident that goes quiet; summarizer.py has run a stale sweep at incident_auto_resolve_minutes (90) the whole time. The 37 open incidents were caused by over-creation, not by a missing sweeper, and 90 minutes may be fine now -- worth rechecking on a fully post-fix dump before changing it. No new environment variables: tg_dispatch_thin_idle_minutes is a config.py default and is not templated into any .env, so CI deploys this without an ansible run. Co-Authored-By: Claude Opus 5 --- drb-c2-core/app/config.py | 9 ++++++++- drb-c2-core/tests/test_correlator_gate.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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: