From aea570db4e21c2012efce042b790980fb6a1978d Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 9 Jul 2026 15:59:50 -0500 Subject: [PATCH] fix(kanban): clear failure/crash diagnostics while a retry is in flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A retried task (→ running) kept showing "crashed Nx": the in-flight run has no outcome yet, so the trailing crash scan skipped it and kept counting the prior streak, and the consecutive_failures counter lingers. Exempt `running` from both repeated_failures and repeated_crashes so a fresh attempt clears the banner until it itself resolves (re-fires if the new run also fails). --- hermes_cli/kanban_diagnostics.py | 15 +++++++++++++-- tests/hermes_cli/test_kanban_diagnostics.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/hermes_cli/kanban_diagnostics.py b/hermes_cli/kanban_diagnostics.py index 1ee680f1e40..8173f1e3338 100644 --- a/hermes_cli/kanban_diagnostics.py +++ b/hermes_cli/kanban_diagnostics.py @@ -535,8 +535,14 @@ def _rule_repeated_failures(task, events, runs, now, cfg) -> list[Diagnostic]: to retry, so a lingering failure streak is history, not a signal. (``complete_task`` resets the counter, but a manual done — e.g. a dashboard drag — ends no run and used to leave the flag stuck.) + + A fresh attempt in flight (``running``) is also exempt: retrying a + task should clear the stale failure banner until this attempt also + resolves. Otherwise a card that's actively trying again still shows + "failed Nx", which reads as a current failure. It re-fires if the new + run fails too (status leaves ``running`` with a recorded outcome). """ - if _task_field(task, "status") in ("done", "archived"): + if _task_field(task, "status") in ("done", "archived", "running"): return [] threshold = _positive_int(cfg.get( "failure_threshold", @@ -662,8 +668,13 @@ def _rule_repeated_crashes(task, events, runs, now, cfg) -> list[Diagnostic]: history, and a manual done (dashboard drag) appends no ``completed`` run to break the crash streak, so the flag was permanent (#kanban desktop dogfood). Done means done. + + ``running`` is exempt too: a fresh attempt is in flight, and its + in-flight run (no outcome yet) doesn't break the trailing crash scan, + so a retried card kept showing "crashed Nx" over an active run. The + banner re-fires if the new attempt also crashes. """ - if _task_field(task, "status") in ("done", "archived"): + if _task_field(task, "status") in ("done", "archived", "running"): return [] failure_threshold = int(cfg.get( "failure_threshold", diff --git a/tests/hermes_cli/test_kanban_diagnostics.py b/tests/hermes_cli/test_kanban_diagnostics.py index c040bbdfeef..474cfd07611 100644 --- a/tests/hermes_cli/test_kanban_diagnostics.py +++ b/tests/hermes_cli/test_kanban_diagnostics.py @@ -282,6 +282,17 @@ def test_failure_rules_exempt_terminal_statuses(): assert kd.compute_task_diagnostics(task, [], runs) == [] +def test_failure_rules_exempt_running_retry(): + # Retrying a task (→ running) puts a fresh attempt in flight; its + # in-flight run (no outcome) doesn't break the trailing crash scan, + # so the past streak used to keep flagging over an active retry. + # A running card must clear the failure/crash banner until this + # attempt itself resolves. + runs = [_run(outcome="crashed", run_id=1), _run(outcome="crashed", run_id=2)] + task = _task(status="running", assignee="crashy", consecutive_failures=3) + assert kd.compute_task_diagnostics(task, [], runs) == [] + + def test_stuck_in_blocked_fires_past_threshold(): now = int(time.time()) task = _task(status="blocked")