Skip to content

A scheduled recording now makes sure the computer is awake for it - #1400

Merged
accesswatch merged 2 commits into
mainfrom
feat/scheduled-recording-wake
Aug 16, 2026
Merged

A scheduled recording now makes sure the computer is awake for it#1400
accesswatch merged 2 commits into
mainfrom
feat/scheduled-recording-wake

Conversation

@accesswatch

Copy link
Copy Markdown
Contributor

A listener scheduled a football pregame for 11:00 and Quill Radio announced the recording at 11:03. Nothing failed, nothing was logged, and he lost the first three minutes with no way to find out why.

RecordingScheduler polls every 20 seconds, so 20 seconds is the entire budget — minutes mean the machine slept. A schedule is a thread inside a running app, and a sleeping computer runs no threads. Keep-awake was scoped to (playing or recording), covering every moment except the quiet stretch before a scheduled recording — exactly when an idle computer decides to sleep.

Three defences, weakest to strongest

  1. It says so. The scheduling window states the requirement in one line, before anything is set: Quill Radio must be running, and the tray counts.
  2. It holds standby off as the moment approaches. schedule_wake.is_imminent, five minutes ahead, consulted by the sleep inhibitor alongside playback and recording — re-evaluated on the existing one-minute tick, because "a recording is now five minutes away" is not a playback state change and would never otherwise reach it.
  3. It can wake the machine. Nothing inside a sleeping process can rouse it. recording_wake_task registers a one-shot per-user Task Scheduler entry with WakeToRun two minutes ahead, then starts Quill Radio if it is not running. From XML, because schtasks /Create has no switch for that flag; DisallowStartIfOnBatteries off so a laptop still records; re-registered on every schedule change and at launch, removed when nothing is scheduled.

Two RadioHistory flags, both default true and deliberately separate: inhibiting standby is local and permission-free, waking the machine changes how the computer behaves. Best effort throughout — a machine whose policy blocks schtasks keeps the other two defences and sees no error.

GATE-11 forced two extractions rather than a rebaseline: apps/radio_preferences.py (the whole Preferences dialog out of the at-ceiling app shell) and the new logic into its own module rather than the at-ceiling recording_schedule.py.

Second commit: a test was writing into a real user profile

test_radio_favorites_manual_order patches quill.core.paths.app_data_dir. main_frame_radio imported the name at module scope, so it holds its own binding the patch never touches — and _save_radio_favorites reads that one. The test wrote its fixture stations ("Zeta", "Alpha", "Mu" at example.com) into a real radio_favorites.json, replacing seven actual stations. The suite passed every time; backups show ~two weeks of this, found only because somebody looked at their favorites and asked what "Zeta" was.

The test now patches the binding that is read — and tests/conftest.py refuses the real profile outright, so any write beneath it raises and names the path. _DEV_BUILD + QUILL_DATA_DIR isolates a test that remembers to ask; this catches the test that thinks it did, which is the only kind that loses data quietly. Verified by probing a real-profile write (blocked, no file created) and by a full suite run.

Verification

  • 11 new tests over the wake arithmetic against an injected clock, including the exact reported shape: imminent at 10:58, not at 10:50.
  • Full CI-shaped suite (-n 4 --dist=loadfile) green; the two failures it surfaced are fixed here.
  • ruff, format, mypy (809 files), GATE-11, banned patterns, error codes, docs parity all clean. Dialog/accessible-name/public-surface inventories regenerated: no drift.
  • Both apps launch clean.

🤖 Generated with Claude Code

Jeff Bishop and others added 2 commits August 15, 2026 17:13
…ke for it

Reported by a listener, and invisible from inside the app: he scheduled a
football pregame for 11:00 and Quill Radio announced the recording at 11:03.
Nothing failed. Nothing was logged. He lost the first three minutes of the show
and had no way to find out why.

`RecordingScheduler` polls every twenty seconds, so twenty seconds is the whole
budget -- minutes mean the machine was asleep. A schedule is a thread inside a
running app, and a sleeping computer runs no threads: nobody asks the schedule
anything until it wakes, and the window model then fires the entry immediately,
which from the outside is indistinguishable from the app losing track of time.
Keep-awake was scoped to `(playing or recording)`, covering every moment except
the quiet stretch before a scheduled recording -- which is exactly when a
computer with nothing to do decides to sleep.

Three defences, weakest to strongest, meant to work together:

**It says so.** The scheduling window states the requirement in one line before
anything is set: Quill Radio must be running, and the tray counts. A requirement
you discover by losing the start of a game is not a requirement, it is a trap.

**It holds standby off as the moment approaches.** `schedule_wake.is_imminent`
answers the question the sleep inhibitor now asks alongside playback and
recording, five minutes ahead. Re-evaluated on the existing one-minute tick,
because "a recording is now five minutes away" is not a playback state change
and would otherwise never reach the inhibitor.

**And it can wake the machine.** Nothing inside a sleeping process can rouse it;
only the OS can. `recording_wake_task` registers a one-shot per-user Task
Scheduler entry with `WakeToRun` two minutes ahead, which then starts Quill
Radio if it is not already running. Registered from XML because `schtasks
/Create` has no switch for that flag, with `DisallowStartIfOnBatteries` off so a
laptop still records. Re-registered whenever the schedule changes and at launch
(a one-shot task is spent once it fires), removed when nothing is scheduled, so
Task Scheduler never fills with dead entries.

Two `RadioHistory` flags, both default true and separate on purpose: inhibiting
standby is local and permission-free, while waking the machine changes how the
computer behaves, and one switch could not express wanting only the first.
Everything is best effort and never raises -- a machine whose policy blocks
`schtasks` keeps the other two defences and sees no error.

The pure arithmetic is tested against an injected clock, including the exact
shape of the report: at 10:58 an 11:00 recording is imminent, at 10:50 it is
not. GATE-11 forced two extractions rather than a rebaseline:
`apps/radio_preferences.py` (the whole Preferences dialog, out of the
at-ceiling app shell) and this logic into its own module rather than the
at-ceiling `recording_schedule.py`.

Already true and now documented: a late start does not run late at the other end
-- `remaining_minutes` records the time *left* in the window -- which is why a
late start costs the beginning, and why a cushion means starting earlier *and*
lengthening by the same amount.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…file

`test_radio_favorites_manual_order` isolates itself by patching
`quill.core.paths.app_data_dir`. `main_frame_radio` does
`from quill.core.paths import app_data_dir` at module scope, so it holds its own
binding that the patch never touches -- and `_save_radio_favorites` resolves the
name from *there*. The test therefore wrote its three fixture stations ("Zeta",
"Alpha", "Mu", at example.com) straight into a real `radio_favorites.json`,
replacing seven actual stations. The suite passed every time it did so; backups
show it went unnoticed for about two weeks, and it surfaced only because
somebody looked at their favorites and asked what "Zeta" was.

Two fixes, because the test is the symptom and the missing guard is the cause.

The test now patches the binding that is actually read.

And `tests/conftest.py` refuses the real profile outright: any write beneath the
directory the app would use raises, naming the path and pointing at the
`quill_data_dir` fixture. `_DEV_BUILD` plus `QUILL_DATA_DIR` already isolates a
test that remembers to ask; this catches the test that *thinks* it did, which is
the only kind that can lose somebody's data quietly. Verified by probing a write
to the real profile and watching it fail with no file created, then by a full
suite run to confirm no legitimate test was relying on writing there.

Also here: `meeting*.md` join `john.md` in the scratch-file ignore block, so the
repo-layout gate stops failing on working notes at the repository root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@accesswatch
accesswatch merged commit d7e42a0 into main Aug 16, 2026
13 checks passed
@accesswatch
accesswatch deleted the feat/scheduled-recording-wake branch August 16, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant