fix(webui): address #905 REQUEST_CHANGES on notifications classifier
B1: classify_attention_event uses structured flags/category only — never substring-match human-authored title/summary for escalation. B2: make notification ids unique across probe_errors and collisions (include loop index / kind). B3: do not assign probe_errors to fetch_error (avoids false Fetch Warning and double-reporting). Regression tests cover all three blockers. Refs #648
This commit is contained in:
+17
-11
@@ -7,13 +7,12 @@ operators receive direct alerts only for human-required escalation boundaries
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Sequence
|
||||
from typing import Any, Callable
|
||||
|
||||
from webui import console_redaction
|
||||
from webui.project_registry import find_project, load_registry
|
||||
from webui.project_registry import load_registry
|
||||
from webui.queue_loader import QueueSnapshot, load_queue_snapshot
|
||||
from webui.lease_loader import LeaseSnapshot, load_lease_snapshot
|
||||
from webui.system_health import SystemHealthSnapshot, load_system_health
|
||||
@@ -158,7 +157,13 @@ def classify_attention_event(
|
||||
2. Operational queues (blocker, stale lease, unassigned ready work, queue collision)
|
||||
-> ATTENTION_OPERATOR (requires_human=False).
|
||||
3. Routine state transitions (clean progression, healthy heartbeats) -> ATTENTION_ROUTINE (requires_human=False).
|
||||
|
||||
Classification uses structured flags and category only. Human-authored
|
||||
``title`` / ``summary`` text is never substring-matched for escalation
|
||||
(PR #905 review B1) — callers that need text signals must set flags from
|
||||
machine-generated status/detail fields before calling this function.
|
||||
"""
|
||||
del title, summary # kept for API stability; never used for classification
|
||||
if (
|
||||
is_hard_stop
|
||||
or is_auth_failure
|
||||
@@ -166,9 +171,6 @@ def classify_attention_event(
|
||||
or is_decision_lock
|
||||
or is_validation_failure
|
||||
or category in {CATEGORY_AUTH, CATEGORY_VALIDATION}
|
||||
or "hard stop" in summary.lower()
|
||||
or "unauthorized" in summary.lower()
|
||||
or "irrecoverable" in summary.lower()
|
||||
):
|
||||
return ATTENTION_HUMAN_REQUIRED, True
|
||||
|
||||
@@ -234,7 +236,7 @@ def load_notifications_snapshot(
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# 1. System health alerts (highest priority)
|
||||
for probe_err in getattr(health_snap, "probe_errors", ()):
|
||||
for err_idx, probe_err in enumerate(getattr(health_snap, "probe_errors", ())):
|
||||
att_cls, req_human = classify_attention_event(
|
||||
CATEGORY_SYSTEM,
|
||||
"System Health Probe Error",
|
||||
@@ -243,7 +245,7 @@ def load_notifications_snapshot(
|
||||
)
|
||||
items.append(
|
||||
NotificationItem(
|
||||
id=f"notif-sys-err-{project.id}",
|
||||
id=f"notif-sys-err-{project.id}-{err_idx}",
|
||||
attention_class=att_cls,
|
||||
category=CATEGORY_SYSTEM,
|
||||
title="System Health Error",
|
||||
@@ -431,16 +433,18 @@ def load_notifications_snapshot(
|
||||
)
|
||||
)
|
||||
|
||||
for collision in lease_snap.duplicate_prs:
|
||||
for col_idx, collision in enumerate(lease_snap.duplicate_prs):
|
||||
att_cls, req_human = classify_attention_event(
|
||||
CATEGORY_BLOCKER,
|
||||
f"Duplicate PR Collision ({collision.kind})",
|
||||
collision.message,
|
||||
is_blocker=True,
|
||||
)
|
||||
issue_part = collision.issue_number if collision.issue_number is not None else "none"
|
||||
kind_part = (collision.kind or "unknown").replace(" ", "-")
|
||||
items.append(
|
||||
NotificationItem(
|
||||
id=f"notif-collision-{collision.issue_number or 0}",
|
||||
id=f"notif-collision-{kind_part}-{issue_part}-{col_idx}",
|
||||
attention_class=att_cls,
|
||||
category=CATEGORY_BLOCKER,
|
||||
title=f"Collision Alert ({collision.kind})",
|
||||
@@ -459,7 +463,9 @@ def load_notifications_snapshot(
|
||||
operator_count = sum(1 for i in items if i.attention_class == ATTENTION_OPERATOR)
|
||||
routine_count = sum(1 for i in items if i.attention_class == ATTENTION_ROUTINE)
|
||||
|
||||
fetch_err = queue_snap.fetch_error or lease_snap.fetch_error or getattr(health_snap, "probe_errors", None)
|
||||
# Fetch errors are transport/load failures only — not probe results that
|
||||
# already surface as first-class notification items (PR #905 review B3).
|
||||
fetch_err = queue_snap.fetch_error or lease_snap.fetch_error
|
||||
if isinstance(fetch_err, (tuple, list)):
|
||||
fetch_err = "; ".join(fetch_err) if fetch_err else None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user