headless-primary: make the watcher always-converge

Add a self-healing backstop so recovery no longer depends on an event firing:
- periodic reconcile (every 8s): read-only drift check that runs apply ONLY when
  broken (no headless / DP-1 not mirroring / workspace trapped on a mirror /
  orphan / off-screen window) — no steady-state flicker.
- delayed reconcile 1.5s after a monitor event to catch late workspace
  reassignment races.
- socat auto-reconnect; exit cleanly when the session socket vanishes.
New 'reconcile' subcommand. Verified: reconcile heals a deliberately broken
mirror and no-ops on a healthy state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Woodard
2026-07-30 16:33:10 -06:00
parent 0fe38e9797
commit 2bd57a8395
2 changed files with 118 additions and 57 deletions

View File

@@ -49,7 +49,7 @@ clients. Inherent to cloning an ultrawide.
| Path | Role |
|---|---|
| `bin/sunshine-headless-primary.sh` | The manager. `apply` establishes/repairs state (idempotent); `rescue` pulls off-screen floating windows back on-screen; `watch` re-applies on monitor hotplug AND auto-rescues windows that open off-screen. |
| `bin/sunshine-headless-primary.sh` | The manager. `apply` establishes/repairs state (idempotent); `rescue` pulls off-screen floating windows back on-screen; `reconcile` heals drift only if broken; `watch` the self-healing daemon (events + periodic reconcile backstop + auto-reconnect). |
| `bin/sunshine-prestart.sh` | Sunshine `ExecStartPre` — thin wrapper that runs `sunshine-headless-primary.sh apply` before the encoder probe. |
| `~/.config/hypr/monitors.conf` | `DP-1` kept **normal** (safety fallback); `HEADLESS-1..4` default to `0x0`. The manager applies the `DP-1 → mirror HEADLESS-1` on top. |
| `~/.config/hypr/autostart.conf` | `exec-once` runs `apply` at login and starts the `watch` daemon. |
@@ -77,16 +77,28 @@ clients. Inherent to cloning an ultrawide.
### Self-healing (the watcher)
`watch` subscribes to Hyprland's event socket and reacts:
`watch` is designed so the setup **always converges**, not just when an event
fires. Three layers:
- `monitoradded` / `monitorremoved` → full `apply` (re-impose the mirror,
re-consolidate, rescue) — debounced 2s to coalesce the v1+v2 event pair.
- `openwindow` → targeted `rescue` of just that window if it opened off-screen
(0.3s settle delay first).
1. **Events** (fast path) — subscribes to Hyprland's socket:
- `monitoradded` / `monitorremoved``apply` immediately (debounced 2s to
coalesce the v1+v2 burst), then a `reconcile` 1.5s later to catch a
workspace Hyprland reassigns to `DP-1` *after* apply already ran.
- `openwindow` → targeted `rescue` if that window opened off-screen.
2. **Periodic `reconcile` backstop** (every `RECONCILE_SECS`, default 8s) — a
read-only drift check that runs `apply` **only when something is actually
wrong** (no headless / DP-1 not mirroring / workspace trapped on a mirror /
orphaned workspace / off-screen window). This is the guarantee: even if an
event is missed entirely, the state self-heals within a few seconds. It does
nothing when healthy, so there's no steady-state flicker.
3. **Auto-reconnect** — if `socat` drops, the loop reconnects; if the session
socket disappears (Hyprland gone) it exits cleanly so the next login's
`exec-once` starts a fresh watcher (no cross-session duplicates).
This is what makes stray windows self-heal: a monitor swap or an app that opens
off-screen (OBS, `bluetui`, kdenlive have all done this) gets pulled back into
view automatically, no manual hunting.
So a monitor swap, a trapped workspace ("can't show workspace 1"), or an app
that opens off-screen (OBS, `bluetui`, kdenlive have all done this) gets fixed
automatically — near-instant via the event, or within ~8s via the backstop.
Force a check manually anytime with `sunshine-headless-primary.sh reconcile`.
### Critical implementation notes