Root-caused the 2026-08-21 stuck-lock incident: a manual workout
unlocked correctly, but the mechanism behind "locked again seconds
later" was a stray workout-locker.service re-arm racing a second
process into building a competing lock window -- not a leetcode-guard
conflict as first suspected (its timer wasn't due until 13:00 that
day). Separately, diet_guard held the global grab for ~3 minutes
against a newly-arriving, higher-ranked screen_locker that morning,
which just retried and logged instead of making progress.
- _build_lock_window now calls gatelock.wait_for_turn before arming,
so a stronger incumbent is queued behind instead of raced.
- New screen_locker/_instance.py (flock-based, mirrors
leetcode_guard/diet_guard's own single-instance guards) stops a
second process from ever building a competing window in the first
place.
- Bumped to gatelock v0.7.1, which adds weaker-holder preemption
(SIGTERM, reusing the holder's own clean close() path) so a lower-
ranked incumbent can no longer block a stronger claim for minutes.
- Fixed a live crash in scripts/verify_screen_fits.py: it read the
real log.json, so today's exhausted manual-workout budget (itself
caused by the incident's duplicate entry) made the fit-check crash
on an unset _mw_error_label. Points at a path that can never exist
instead, so the measurement no longer depends on live data.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk