Skip to content

fix(monitoring): health-check the whole fleet, not just running agents (#675) - #853

Merged
vybe merged 1 commit into
devfrom
feature/675-fleet-health-isolation
May 15, 2026
Merged

fix(monitoring): health-check the whole fleet, not just running agents (#675)#853
vybe merged 1 commit into
devfrom
feature/675-fleet-health-isolation

Conversation

@dolho

@dolho dolho commented May 15, 2026

Copy link
Copy Markdown
Contributor

Root cause

#669 patched the user-visible 500 (NULL status coercion). This is the actual cause of the stale 24h rollups: _run_check_cycle filtered the fleet to running agents before the 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 dropped from every cycle, so its agent_health_checks row 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 passes all=True (stopped containers included, 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: 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

Ask Outcome
Confirm per-agent isolation, add if missing Already presentperform_fleet_health_check uses asyncio.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.
Structured per-pass log Added — one INFO line/cycle: fleet_health_check pass complete: agents=N (running=R stopped=S) healthy=.. degraded=.. unhealthy=.. critical=.. unknown=.. duration_ms=..
Audit registration path No separate registry — cycle re-derives the fleet from list_all_agents_fast() each pass. "Not registered" reduces to this filter.
Pull prod logs around stale timestamps Not done — no production access from this environment.

Tests

tests/unit/test_fleet_check_covers_all_agents.py (4):

  • running + stopped both checked (root-cause regression guard)
  • all-stopped fleet still checked (the exact months-stale prod shape — pre-fix returned early)
  • zero-agent no-op
  • pass-summary log emitted with correct running/stopped/status counts

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

#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>
@dolho

dolho commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

Live-verified on the running instance (real _run_check_cycle() against live Docker + DB, fleet of 5: 1 running, 4 stopped):

Structured log fired with real data:
fleet_health_check pass complete: agents=5 (running=1 stopped=4) healthy=0 degraded=0 unhealthy=3 critical=2 unknown=0 duration_ms=484

Stopped agent Circuit Outcome
guardrail-test closed ✅ 4 fresh agent_health_checks rows (had none before)
my-new-agent closed ✅ 4 fresh rows
another_test dormant no rows — #631 synthetic short-circuit (unmodified, by design)
pr-reviewer dormant no rows — same

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 unhealthy/dormant with last_check_at=now, only the historical rows lag) is the documented, intentional boundary — now confirmed empirically rather than just by analysis. Modifying it is explicitly out of #675 scope (reopens the SQLite contention #631 fixed).

@dolho
dolho requested a review from vybe May 15, 2026 13:21

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix. Root cause clearly identified, minimal change, #631 dormant-skip explicitly preserved, 4 targeted regression tests. One note: consider updating the issue link from 'Related to' to 'Closes #675' if no further follow-up is planned on #675.

@vybe
vybe merged commit ba9d2d0 into dev May 15, 2026
9 checks passed
AndriiPasternak31 added a commit that referenced this pull request May 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants