A launchd slot runs sak-runner as an Interactive job, and the runner keeps its host awake while it holds a run or a job #187

Merged
agra merged 4 commits from task/3744-on-a-macos-runner-at-night-timers-stretc into master 2026-10-30 03:55:59 +02:00
Owner

Tracker: #3744

What it delivers

  • Each slot's launchd agent sets ProcessType Interactive (runner/slot.sx). launchd throttles an agent with no ProcessType. While no one uses the Mac, that agent's timers run late, and so do the timers of every process it starts: harnesses, gates and their tests. An Interactive job runs with an app's resource limits, which is none. An update's new version writes its own slot's agent (--start-slot), so the first update to this release already runs as an Interactive job.
  • On macOS, the supervisor keeps /usr/bin/caffeinate -i -w <runner pid> running while a run or a job is active (runner/supervisor.sx). This stops an unattended host from idle-sleeping under the work. The main thread starts it when the first run or job starts and ends it when the last one ends. caffeinate also ends if the runner exits. A sign-in does not hold it. config.macos is the platform rule; agy's unrunnable uses it as well.

The task also names a clock-timed tail heartbeat. That is already on master (fa0995c7).

Why the job type, and not the assertion, fixes the timers

Measured on s32 at 02:10–02:40 local on 2026-10-30, with no user input for 3.3–3.8 h (HIDIdleTime), using the task's repro (20 × time.sleep(0.1), mean seconds per sleep):

where the repro ran mean sleep ps -o pri
a scratch launchd agent with no ProcessType 0.225 20
a scratch agent with ProcessType Standard 0.227 20
a scratch agent with ProcessType Background 0.277 4
a scratch agent with ProcessType Interactive 0.109 31
this run's shell, under s32's live runner 0.22, 0.195 20
the same shell under caffeinate -i 0.232
the same shell under taskpolicy -l 0 -t 0, or -a 0.232, 0.239

s32 also holds a system-wide PreventUserIdleSystemSleep assertion (Amphetamine), and its timers still stretch. So a power assertion does not give timer accuracy. The job type does. The assertion here does a different job: it keeps a host whose energy settings allow idle sleep awake under a run.

Through the runner itself, at 02:37 local: tests/runner_launchd.sx arms whose stub harness ran the repro in its gated step, under the scratch slot's real launchd agent:

  • head: 0.108 s, priority 31;
  • master: 0.235 s, priority 20.

Regressions, failing before and passing after

Each was run from a git archive arm of master with this branch's tests:

  • tests/runner_launchd.sx: "slot a's step's harness runs at an app's priority, 31, which launchd gives an Interactive job, not the 20 of an agent it throttles". Master: RUNNER LAUNCHD FAIL (1 checks). Head: RUNNER LAUNCHD OK. The stream-json harness stub now records its pid to ./pid.log, and the test reads that process's priority with ps. Unlike the timers, the priority does not depend on the time of day.
  • tests/runner_install.sx: "macOS: the agent is an Interactive job, which launchd does not throttle". Master: RUNNER INSTALL FAIL (1 checks). Head: OK.
  • tests/runner_core.sx check_awake: a supervisor of capacity 2 runs two chattering runs. While both go, the supervisor's process runs exactly one /usr/bin/caffeinate -i -w <its pid>, and pmset -g assertions lists that process with PreventUserIdleSystemSleep. Once the supervisor is done with the first run, that caffeinate still runs. Once both runs end, no caffeinate child is left. Master fails 3 checks (RUNNER CORE FAIL (3 checks)). A mutation arm that drops if busy { return; } from rest, so the caffeinate ends with every run, fails 1: "once the first of the two runs ends, that caffeinate still runs". Head: RUNNER CORE OK.
  • tests/sign_in.sx check_pasted: while a sign-in's login is up under the supervisor, the runner's process runs no caffeinate, as a sign-in keeps no host awake. A mutation arm that starts the caffeinate for every ticket, sign-ins included, and keeps it while any slot is active fails exactly that check (SIGN IN FAIL (1 checks)). Head: SIGN IN OK. Master passes it too, since master starts no caffeinate at all. The check pins the sign-in rule, not the bug.

Golden set

None: this changes nothing under web/.

Gates at the head c3e89d7e

  • full: gate run 875, clean, passed. That covers all 69 tests/*.sx files, release.sh with step9, the deploy contract, the main Playwright config (777 passed, 1 skipped) and the runner config (10 passed).
Tracker: [#3744](https://sak.swipelab.com/tasks/3744) ## What it delivers - **Each slot's launchd agent sets `ProcessType` `Interactive`** (`runner/slot.sx`). launchd throttles an agent with no `ProcessType`. While no one uses the Mac, that agent's timers run late, and so do the timers of every process it starts: harnesses, gates and their tests. An `Interactive` job runs with an app's resource limits, which is none. An update's new version writes its own slot's agent (`--start-slot`), so the first update to this release already runs as an Interactive job. - **On macOS, the supervisor keeps `/usr/bin/caffeinate -i -w <runner pid>` running while a run or a job is active** (`runner/supervisor.sx`). This stops an unattended host from idle-sleeping under the work. The main thread starts it when the first run or job starts and ends it when the last one ends. caffeinate also ends if the runner exits. A sign-in does not hold it. `config.macos` is the platform rule; agy's `unrunnable` uses it as well. The task also names a clock-timed tail heartbeat. That is already on master (fa0995c7). ## Why the job type, and not the assertion, fixes the timers Measured on s32 at 02:10–02:40 local on 2026-10-30, with no user input for 3.3–3.8 h (`HIDIdleTime`), using the task's repro (20 × `time.sleep(0.1)`, mean seconds per sleep): | where the repro ran | mean sleep | `ps -o pri` | | --- | --- | --- | | a scratch launchd agent with no `ProcessType` | 0.225 | 20 | | a scratch agent with `ProcessType` `Standard` | 0.227 | 20 | | a scratch agent with `ProcessType` `Background` | 0.277 | 4 | | a scratch agent with `ProcessType` `Interactive` | **0.109** | 31 | | this run's shell, under s32's live runner | 0.22, 0.195 | 20 | | the same shell under `caffeinate -i` | 0.232 | | | the same shell under `taskpolicy -l 0 -t 0`, or `-a` | 0.232, 0.239 | | s32 also holds a system-wide `PreventUserIdleSystemSleep` assertion (Amphetamine), and its timers still stretch. So a power assertion does not give timer accuracy. The job type does. The assertion here does a different job: it keeps a host whose energy settings allow idle sleep awake under a run. Through the runner itself, at 02:37 local: `tests/runner_launchd.sx` arms whose stub harness ran the repro in its `gated` step, under the scratch slot's real launchd agent: - head: **0.108 s**, priority 31; - master: **0.235 s**, priority 20. ## Regressions, failing before and passing after Each was run from a `git archive` arm of master with this branch's tests: - `tests/runner_launchd.sx`: "slot a's step's harness runs at an app's priority, 31, which launchd gives an Interactive job, not the 20 of an agent it throttles". Master: `RUNNER LAUNCHD FAIL (1 checks)`. Head: `RUNNER LAUNCHD OK`. The stream-json harness stub now records its pid to `./pid.log`, and the test reads that process's priority with `ps`. Unlike the timers, the priority does not depend on the time of day. - `tests/runner_install.sx`: "macOS: the agent is an Interactive job, which launchd does not throttle". Master: `RUNNER INSTALL FAIL (1 checks)`. Head: OK. - `tests/runner_core.sx` `check_awake`: a supervisor of capacity 2 runs two chattering runs. While both go, the supervisor's process runs exactly one `/usr/bin/caffeinate -i -w <its pid>`, and `pmset -g assertions` lists that process with `PreventUserIdleSystemSleep`. Once the supervisor is done with the first run, that caffeinate still runs. Once both runs end, no caffeinate child is left. Master fails 3 checks (`RUNNER CORE FAIL (3 checks)`). A mutation arm that drops `if busy { return; }` from `rest`, so the caffeinate ends with every run, fails 1: "once the first of the two runs ends, that caffeinate still runs". Head: `RUNNER CORE OK`. - `tests/sign_in.sx` `check_pasted`: while a sign-in's login is up under the supervisor, the runner's process runs no caffeinate, as a sign-in keeps no host awake. A mutation arm that starts the caffeinate for every ticket, sign-ins included, and keeps it while any slot is active fails exactly that check (`SIGN IN FAIL (1 checks)`). Head: `SIGN IN OK`. Master passes it too, since master starts no caffeinate at all. The check pins the sign-in rule, not the bug. ## Golden set None: this changes nothing under `web/`. ## Gates at the head c3e89d7e - `full`: gate run 875, clean, passed. That covers all 69 `tests/*.sx` files, release.sh with step9, the deploy contract, the main Playwright config (777 passed, 1 skipped) and the runner config (10 passed).
agra merged commit 39594b312d into master 2026-10-30 03:55:59 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/sak!187
No description provided.