From 061a18300837351751b3c6028597d25ef5fe6665 Mon Sep 17 00:00:00 2001 From: baocin <5463986+baocin@users.noreply.github.com> Date: Wed, 6 May 2026 18:18:56 -0500 Subject: [PATCH] fix(kanban): guard task_age against corrupt created_at values like '%s' task_age() crashed with ValueError when created_at contained the literal format string '%s' instead of a Unix timestamp, taking down the entire GET /board endpoint with a 500. - Add _safe_int() helper that returns None on non-numeric values - Refactor task_age() to use _safe_int instead of bare int() casts - Wrap task_age() call in _task_dict with try/except fallback so one corrupt row never kills the whole board endpoint --- hermes_cli/kanban_db.py | 22 ++++++++++++++++------ plugins/kanban/dashboard/plugin_api.py | 5 ++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 09a9db8534c..2408a4a7762 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -4190,16 +4190,26 @@ def board_stats(conn: sqlite3.Connection) -> dict: } +def _safe_int(val: Optional[str]) -> Optional[int]: + """Parse a timestamp field to int, returning None on garbage like '%s'.""" + if val is None: + return None + try: + return int(val) + except (ValueError, TypeError): + return None + + def task_age(task: Task) -> dict: """Return age metrics for a single task. All values are seconds or None.""" now = int(time.time()) - age_since_created = now - int(task.created_at) if task.created_at else None - age_since_started = ( - now - int(task.started_at) if task.started_at else None - ) + created = _safe_int(task.created_at) + started = _safe_int(task.started_at) + completed = _safe_int(task.completed_at) + age_since_created = now - created if created else None + age_since_started = now - started if started else None time_to_complete = ( - int(task.completed_at) - int(task.started_at or task.created_at) - if task.completed_at else None + completed - (started or created) if completed else None ) return { "created_age_seconds": age_since_created, diff --git a/plugins/kanban/dashboard/plugin_api.py b/plugins/kanban/dashboard/plugin_api.py index cac563e9418..cc737694394 100644 --- a/plugins/kanban/dashboard/plugin_api.py +++ b/plugins/kanban/dashboard/plugin_api.py @@ -145,7 +145,10 @@ def _task_dict( d = asdict(task) # Add derived age metrics so the UI can colour stale cards without # computing deltas client-side. - d["age"] = kanban_db.task_age(task) + try: + d["age"] = kanban_db.task_age(task) + except Exception: + d["age"] = {"created_age_seconds": None, "started_age_seconds": None, "time_to_complete_seconds": None} # Surface the latest non-null run summary so dashboards don't show # blank cards/drawers for tasks where the worker handed off via # ``task_runs.summary`` (the kanban-worker pattern) instead of