tart_health_agent: fix three bugs that made guest probes collect nothing - #70
Merged
Conversation
Enabling tart_health_guests on 2026-08-17 produced `guest_reachable: false` on 26/26
slots with no guest fields at all, so it was reverted the same day (ronin_puppet
#1336 -> #1338). Three independent bugs, each verified against a live host.
1. GUEST_PROBE was interpolated into a Tcl double-quoted spawn argument. It is
multi-line and contains $(...), $4 and [0-9a-f], so Tcl substituted the $ and
treated [ ] as command substitution:
extra characters after close-quote
while executing "spawn ssh ... "uptime | sed -n 's/.*up \([^,]*\)...
expect died before connecting. Now base64'd: alphanumeric plus +/= is special to
neither Tcl nor the remote shell, whatever the probe grows into later.
2. The password was matched with a single-line braced
expect { -re "(P|p)assword:" { send "admin\r"; exp_continue } timeout { } eof { } }
which never sent it — measured against a real guest it printed the prompt and sat
until the 60s timeout. Replaced with the multi-line glob form plus an explicit
`expect eof`, which returns all five keys.
3. guest_up_s used an unanchored sed on kern.boottime:
{ sec = 1786987025, usec = 7795 } Mon Aug 17 17:17:05 2026
`.*sec = ` is greedy, so it matched "usec = " and captured the MICROSECONDS.
guest_up_s came out as now-7795, i.e. ~1.79e9. Measured 1786979442 on a guest whose
uptime said 4 mins; anchored it reads 263.
Also, separately: the tart pid lookup used
pgrep -f "[t]art run --no-graphics $vm"
and enabling tart.inject_vault fleet-wide changed the command line to
tart run --no-graphics --dir=vault:/Users/admin/.tart-vault/<vm>:ro <vm>
so the pattern matched nothing and tart_run_uptime_s was None on 25/26 slots. Now
anchored on the VM name at end-of-line, which is flag-agnostic.
Verified end to end on macmini-m4-236 with --dry-run: guest_reachable True,
guest_uptime_s 368/616, guest_disk_free_gib 61/45, clock_skew_s 0/-6,
configured_worker_id matching, tart_run_uptime_s 5760/10464.
DO NOT set tart_health_guests back to true on the strength of this alone. Now that
the data arrives, it shows Hangar's reboot-loop heuristic is wrong for this pool:
guest_uptime_s < tart_run_uptime_s * 0.5 (once tart run is up >1h)
These guests are numberOfTasksToRun=1, so they reboot after EVERY task and
guest_uptime is one task (600-2400s) while tart run is days. Both slots above satisfy
the condition and would report CRIT "guest rebooting under a healthy tart run". The
real loop was ~84s. A ratio cannot separate those; it needs an absolute floor, or
better, tasks-resolved-count, which was 0 on every genuinely looping slot. That
threshold lives server-side in hangar (backend/app/api/tart_health.py) and is the
remaining blocker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Enabling
tart_health_guestson 2026-08-17 producedguest_reachable: falseon 26/26 slots withno guest fields at all, so it was reverted the same day (ronin_puppet #1336 → #1338). Three
independent bugs, each verified against a live host.
1. The probe never reached the guest — Tcl ate it
GUEST_PROBEis multi-line and contains$(...),$4,[0-9a-f]. It was interpolated into a Tcldouble-quoted
spawnargument, so Tcl substituted the$and treated[ ]as commandsubstitution:
expect died before connecting. Now base64'd — alphanumeric plus
+/=is special to neither Tcl northe remote shell, whatever the probe grows into later.
2. The password was never sent
expect { -re "(P|p)assword:" { send "admin\r"; exp_continue } timeout { } eof { } }Measured against a real guest this printed the prompt and then sat until the 60 s timeout. The
multi-line glob form with an explicit
expect eofreturns all five keys. Side-by-side onmacmini-m4-236: glob →
probe_ok=1+ disk61;-resingle-line → prompt, then nothing.3.
guest_up_swas an epoch, not a durationsysctl -n kern.boottimeprints:.*sec =is greedy, so it matchedusec =and captured the microseconds.guest_up_sbecame
now - 7795≈ 1.79e9. Measured1786979442on a guest whoseuptimesaid 4 mins; anchoredit reads
263.Also: the pid lookup, broken by the vault rollout
Enabling
tart.inject_vaultfleet-wide changed the command line totart run --no-graphics --dir=vault:/Users/admin/.tart-vault/<vm>:ro <vm>, so--no-graphicsand the VM name stopped beingadjacent, the pattern matched nothing, and
tart_run_uptime_swasNoneon 25/26 slots. Nowanchored on the VM name at end-of-line, which is flag-agnostic.
Verified end to end
--dry-runagainst macmini-m4-236, from a staged copy (deployed tree untouched):guest_reachableguest_uptime_sguest_disk_free_gibclock_skew_start_run_uptime_sThat disk column is the signal we actually wanted — it separates rolled from un-rolled slots.
tart_health_guestson the strength of this aloneNow that the data arrives, it shows Hangar's reboot-loop heuristic is wrong for this pool:
These guests are
numberOfTasksToRun: 1— they reboot after every task, soguest_uptimeis onetask (600–2400 s) while
tart runis days. Both slots above satisfy the condition and wouldreport CRIT "guest rebooting under a healthy tart run". The real loop was ~84 s.
A ratio cannot separate those. It needs an absolute floor, or better
tasks-resolved-count, whichwas
0on every genuinely looping slot. That threshold lives server-side in hangar(
backend/app/api/tart_health.py) and is the remaining blocker — otherwise we trade 24 false warnsfor ~24 false crits, which is worse.
🤖 Generated with Claude Code