Real fixes: split over-long functions (lock-decision chain, manual
sync, validation, phone verification, status), hoist a dead lazy
import, context-manage Xvfb and temp dirs, add encodings, share the
silent-failure argv loop.
lock_grab_handoff read lock._recovery, which gatelock's LockWindow
does not have (its public property is recovery); a contract test now
checks every attribute the handoff reads against the real class.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
Three faults that together locked kuhy out on 2026-09-18 with a workout
on disk and "Manual-workout budget exhausted" on the screen.
1. The football logged at 00:02 on 2026-09-13 was rekeyed to its local
day (keeping workout_id manual:2026-09-12T14:30), published, and pulled
straight back by the 09:40 tick as a new record: _already_ingested only
matched sync_record_id (which a locally logged entry never carries) and
the chokepoint re-derived the id from the corrected date, so the same
workout landed twice. Ingest now files a synced manual under the wire
id it arrived with and recognises an entry whose own workout_id is that
record.
2. count_in_window counted raw manual_workout entries, so the twin took a
second budget slot for a week (2/2) while the weekly count, which goes
through credit_key, saw one. The budget now shares credit_key -- the
rule the docstring says every counter must share -- and distinct
workouts on one day still cost a slot each.
3. _check_today_state_exits returned False (full lock) from the expired
early-bird branch before ever consulting has_logged_today, so a manual
walk entered on the lock screen at 09:32 held for exactly one tick: the
09:35 run locked again ("also=workout_logged_today"), and kept doing so
every 5 minutes while the day's marker lasted, which is how a sick day
got spent over a logged workout. The explain chain already treated the
expired marker as non-terminal; the live ladder now falls through the
same way.
_manual_sync.py went over the 250-line cap and is split: draft
reconstruction lives in _manual_sync_draft.py, re-exported from the
original path so patch targets are unchanged.
Replayed on the real log: count_day_credits("2026-09-13") == 1 while the
old count_in_window said 2; after the fix the status API reports
used_7d=1, exhausted=False. test_manual_rekey_dedup.py fails 3/4 on HEAD.
1334 tests, 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>