HydraExperienceNet.app is currently installed only at enrollment time (via the install-hydra-experiencenet step in recipes/hydraheadflatscreen-macos.yaml). Unlike the agent itself, which auto-updates every 6h via hydrarelease/pkg/updater, the Qt app drifts and needs manual reprovision to get new versions. Today turbo-pancake-76 is on a pre-v6.1.4 build while peppy-dumpling-32 is on v6.1.25 — exactly because peppy was hand-installed.
Proposed: have the agent (cross-platform) own ongoing Qt-app updates on darwin via build-tag-gated files, mirroring the existing moonlight_<os>.go pattern.
pkg/qtapp/qtapp.go // public API: CheckAndUpdate(ctx) error
pkg/qtapp/qtapp_darwin.go // real implementation
pkg/qtapp/qtapp_windows.go // no-op stub
pkg/qtapp/qtapp_linux.go // no-op stub
latestVersion() — GET https://releases.experiencenet.com/hydraexperiencenet/production/latest.jsoninstalledVersion() — read agent-managed state file ~/.hydraheadflatscreen/qt_app_version. Not CFBundleShortVersionString; we observed peppy reads 6.1.0 from plist despite running v6.1.25, so the plist isn't a reliable source of truth on this codebase.install(version) — download HydraExperienceNet-v<ver>.dmg, mount, cp -R over /Applications/HydraExperienceNet.app, xattr -cr, unmount, write state file. macOS allows hot-swap over a running bundle (inode replacement); the running process keeps old code until next launch.coordinateRestart(ctx) — only pkill -f HydraExperienceNet when /api/v1/stream/status == idle. Defer to next idle window if streaming. Cap deferral at e.g. 24h to avoid stuck-on-old-version on always-streaming kiosks.Hook qtapp.CheckAndUpdate(ctx) into the same 6h tick that already drives the agent's self-update. No-op on Windows/Linux.
install-hydra-experiencenet stays for first-time enrollment (initial bundle + state file seed).service_versions heartbeat so the central API doesn't need exec to learn it (see also: empty service_versions for Mac Minis today).Today's task — bringing turbo + cheeky-cactus to peppy's v6.1.25 — has to be done via reprovision/exec because the loop doesn't exist. Once this lands, drift between heads becomes self-healing.