fix(monitoring): health-check the whole fleet, not just running agents (#675) - #853
Conversation
#675) Root cause of #669's stale-rollup symptom. `_run_check_cycle` filtered the fleet to `status == "running"` before handing it to `perform_fleet_health_check`: running_agents = [a.name for a in agents if a.status == "running"] if not running_agents: return A stopped / exited / crashed agent was therefore dropped from every cycle, so its `agent_health_checks` row never refreshed. Production showed 8 of 9 agents last refreshed 2026-02-23..04-28 while the one running agent (competitor-analyzer) stayed current — exactly this filter. A down agent is the case monitoring exists to surface, so the filter had the logic backwards. Fix: check every agent. `list_all_agents_fast()` already passes `all=True` (includes stopped/exited/dead/created, normalised to status="stopped"), and `perform_health_check` already produces a correct "docker exited / unreachable → unhealthy" record for a stopped agent. The #631 dormant short-circuit still bounds cost for hard-down agents (synthetic detail, no DB writes) — explicitly NOT modified here; re-enabling writes for dormant agents would reopen the SQLite-contention #631 fixed. Net behaviour: a long-down agent settles to "unhealthy, circuit dormant, last-checked ≤ dormant window ago" instead of literal months-stale data — correct and actionable. Issue asks addressed: - Per-agent isolation: confirmed already present — `perform_fleet_health_check` uses `asyncio.gather(..., return_exceptions=True)` + per-result handling. Hypothesis #1 (one agent's failure short-circuits the fleet) did NOT hold; the bug was the filter. Documented in the function docstring. - Structured per-pass log: new INFO line per cycle — `fleet_health_check pass complete: agents=N (running=R stopped=S) healthy=.. degraded=.. unhealthy=.. critical=.. unknown=.. duration_ms=..` — so a stalled / partial pass is observable in Vector next time instead of inferred months later from stale rollups. - Registration audit: there is no separate registry — the cycle re-derives the fleet from `list_all_agents_fast()` each pass. The "not registered" hypothesis reduces to this filter. - Prod log pull: not done — no production access from this environment; noted in the PR. Tests: tests/unit/test_fleet_check_covers_all_agents.py (4) — running+stopped both checked, all-stopped fleet still checked (the exact months-stale prod shape), zero-agent no-op, pass-summary log emitted with correct counts. Existing monitoring suite (test_monitoring_dormant_skip / router_signatures / fleet_status_resilience, 15 tests) still green — #631 dormant-skip behaviour unchanged. Related to #675 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Live-verified on the running instance (real Structured log fired with real data:
Pre-fix, all 4 stopped agents were filtered out every cycle (only the 1 running agent checked) — the exact #675 root cause / production symptom. Post-fix, the non-dormant stopped agents now refresh correctly with "unhealthy/stopped" records. The two dormant agents behaving as #631 designed (synthetic detail, no DB write — live aggregate status is still correctly |
…871) - agent-monitoring: fleet check covers stopped agents (#675/#853) + per-pass log - parallel-headless-execution: cgroup-walk sweep for env-stripped orphans (#857) - platform-settings: new Feature Flags section (session tab, voice, workspace #860) - public-agent-links: SITE-001 reverse-proxy retirement (#865) Co-Authored-By: Claude <noreply@anthropic.com>
Root cause
#669 patched the user-visible 500 (NULL
statuscoercion). This is the actual cause of the stale 24h rollups:_run_check_cyclefiltered the fleet to running agents before the health check —A stopped / exited / crashed agent was dropped from every cycle, so its
agent_health_checksrow never refreshed. Production: 8 of 9 agents last refreshed 2026-02-23..04-28; only the one running agent stayed current. The filter had the logic backwards — a down agent is exactly what monitoring should surface.Fix
Check every agent.
list_all_agents_fast()already passesall=True(stopped containers included, normalised tostatus="stopped"), andperform_health_checkalready produces a correct "docker exited / unreachable → unhealthy" record for a stopped agent.The #631 dormant short-circuit still bounds cost for hard-down agents (synthetic detail, no DB writes) — explicitly not modified: re-enabling writes for dormant agents would reopen the SQLite contention #631 fixed. Net behaviour: a long-down agent settles to "unhealthy, circuit dormant, last-checked ≤ dormant-window ago" instead of literal months-stale data — correct and actionable.
Issue asks
perform_fleet_health_checkusesasyncio.gather(..., return_exceptions=True)+ per-result handling. Hypothesis #1 (one failure short-circuits fleet) did not hold; the bug was the filter. Documented in docstring.fleet_health_check pass complete: agents=N (running=R stopped=S) healthy=.. degraded=.. unhealthy=.. critical=.. unknown=.. duration_ms=..list_all_agents_fast()each pass. "Not registered" reduces to this filter.Tests
tests/unit/test_fleet_check_covers_all_agents.py(4):Existing monitoring suite —
test_monitoring_dormant_skip/test_monitoring_router_signatures/test_fleet_status_resilience(15) — still green; #631 dormant-skip behaviour unchanged.Test plan
Related to #675
🤖 Generated with Claude Code