Surface LLM correlation fields in debug view; fix unit-continuity path
/admin/debug/correlation stripped corr_consensus and the corr_llm_* fields
that upload.py's consensus correlator writes onto the call doc, making it
the one tool built to answer "is the LLM correlation tier alive" unable to
answer it (2026-08-19 dump had to infer LLM state from commit dates instead
of reading it off the data). admin.py's _call_summary() now includes
corr_consensus, corr_llm_reasoning, corr_llm_action, corr_rules_action.
The unit-continuity correlation path never wrote corr_matched_units, unlike
fast/single and fast/disambig, so the debug view showed null for a match
that was in fact unit-driven by construction. Now populated unconditionally
on that path (server-26#16).
Also traced the negative corr_incident_idle_min (-4.1 observed) to its root
cause: the re-correlation sweep anchors `now` to the linking call's own
started_at, and that back-dated value was being written straight into the
incident's updated_at, letting it land before the incident's own
started_at. Added _floor_at_started_at() so updated_at can never precede
started_at. (commit 33a247d already fixed the recency *gates* misreading
that negative value; this fixes the write that produced it.) Verified the
skip_reason filter in recorrelation_sweep.py:63 is already correct, no
change needed there.
Added tests for the debug endpoint's LLM field passthrough, the
unit-continuity corr_matched_units fix, and the updated_at floor — each
confirmed to fail when its fix is reverted. 148 passed, 0 failed.
Closes logan/server-26#24
Closes logan/server-26#16
This commit is contained in:
@@ -278,6 +278,32 @@ def _idle_gate_minutes(inc: dict, now: datetime) -> float:
|
||||
return abs(_incident_idle_minutes(inc, now))
|
||||
|
||||
|
||||
def _floor_at_started_at(inc: dict, when: datetime) -> datetime:
|
||||
"""
|
||||
Clamp a candidate `updated_at` timestamp so it can never land before the
|
||||
incident's own `started_at`.
|
||||
|
||||
The re-correlation sweep anchors `now` to the linking call's own
|
||||
`started_at` (server-26#24 / recorrelation_sweep.py) so that its window
|
||||
math is correct regardless of when the sweep happens to run. But that
|
||||
same back-dated `now` was also being written straight into `updated_at`
|
||||
here — so an orphan whose real-world `started_at` predates the incident's
|
||||
own `started_at` could set `updated_at` earlier than `started_at`,
|
||||
producing the negative `corr_incident_idle_min` observed on 2026-08-19
|
||||
(commit 33a247d fixed the *gates* misreading that negative value, not
|
||||
this write). `started_at` is never rewritten after creation, so it's a
|
||||
safe floor: activity can never honestly be older than the incident itself.
|
||||
"""
|
||||
try:
|
||||
raw = inc.get("started_at") or ""
|
||||
started = datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
|
||||
if started.tzinfo is None:
|
||||
started = started.replace(tzinfo=timezone.utc)
|
||||
except Exception:
|
||||
return when
|
||||
return max(when, started)
|
||||
|
||||
|
||||
def _incident_span_minutes(inc: dict, now: datetime) -> float:
|
||||
"""
|
||||
Wall-clock minutes the incident has been open: from its `started_at` to the
|
||||
@@ -849,6 +875,11 @@ def _run_decision(ctx: dict) -> dict:
|
||||
corr_debug = {
|
||||
"corr_path": "unit-continuity",
|
||||
"corr_incident_idle_min": round(_incident_idle_minutes(best_unit_inc, now), 1),
|
||||
# Unlike this file's other two paths (fast/single, fast/disambig),
|
||||
# a match here is unit-driven by construction (call_unit_set &
|
||||
# _unit_keys(...) is what built unit_candidates), so this is
|
||||
# always populated rather than gated on fit_signal (server-26#16).
|
||||
"corr_matched_units": _matching_units(call_units, best_unit_inc.get("units")),
|
||||
}
|
||||
logger.info(
|
||||
f"Correlator unit-continuity: call {call_id} → "
|
||||
@@ -1531,7 +1562,7 @@ async def _update_incident(
|
||||
# acknowledging. The incident now ages from its last SUBSTANTIVE call, and
|
||||
# thin traffic rides along without extending its life.
|
||||
if refresh_activity:
|
||||
updates["updated_at"] = now.isoformat()
|
||||
updates["updated_at"] = _floor_at_started_at(inc, now).isoformat()
|
||||
else:
|
||||
updates["last_thin_at"] = now.isoformat()
|
||||
if best_location:
|
||||
|
||||
Reference in New Issue
Block a user