Halve the tier-2 thin-call window, from 10 minutes to 5
Build & Deploy / Build & push images (push) Successful in 4m1s
Build & Deploy / Deploy to VM (push) Successful in 2m14s

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 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-08-16 20:56:58 -04:00
co-authored by Claude Opus 5
parent 0bd92269d2
commit 7b5258cfdf
2 changed files with 26 additions and 1 deletions
+8 -1
View File
@@ -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
+18
View File
@@ -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: