Repository navigation
Heat: end the session after 15 minutes of serious heat, and sleep a closed lid - #81
aldikosh23 wants to merge 12 commits into
Conversation
macOS asks for a closed lid's sleep once, at the close, and disablesleep 1 refuses it. disablesleep 0 does not make it ask again, so a Mac ended for heat or a dying battery kept running in the bag until the lid opened. Claude-Session: https://claude.ai/code/session_01Jco1ESE6NxipTXFsZE31rZ
After a failed disablesleep 0, macOS refuses the request, and the recovery agent that later restores sleep does not ask. A refused request now asks again every 30 s while the lid stays closed and no session starts. Also tests an end that cannot remove session.json and a quit during an end. Claude-Session: https://claude.ai/code/session_01Jco1ESE6NxipTXFsZE31rZ
main's README is now short (krishhgg#65) and its lid-close detail lives in docs/lid-close.md, so this PR's sentence about a battery or heat end with the lid closed moves there. The README's battery and heat paragraph gets one short line saying the Mac goes to sleep. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Fixes the three review findings on the retry. The sleep is now owed from the floor end, not tied to one end's reason, so a quit that replaces the pending end no longer drops it. It is asked only with no session, no lifecycle operation queued or running, and no live sudo pmset, so it never lands inside a queued start. A start clears it only once it holds the lock, so a start refused for a busy lock keeps it. Claude-Session: https://claude.ai/code/session_01Jco1ESE6NxipTXFsZE31rZ
caae952 to
2a1b26c
Compare
paoValle
left a comment
There was a problem hiding this comment.
Reviewed the new commit only (2a1b26c), since the draft carries #61's and #80's commits until they land. Source review only — no Xcode on this machine, so I did not run swift test.
What I checked in the new content:
- the ordering in
evaluate:criticalstill wins, and the sustained end sits above the unreadable-battery and low-power branches, so a Mac at serious heat does not first switch Low Power Mode on and then end; HeatLognow stores the start of the stretch and clears it on any reading below serious, andsecondsUntilSeriousThermalEnd()returns nil once the limit is reached, so a refused end is not re-scheduled every second — the retry belongs toSessionManager, which matches the comment;recheckis rebuilt on every run, so a Settings change during a stretch (the limit itself is infloorInputs) re-times the wait, andstop()cancels it;normalizeSeriousThermalLimit(), the write-back and the clamp mirrornormalizeFloors, and a config without the key gets the default;sleepsAClosedLidincludes the new reason, so the lid-closed path goes to sleep like a critical end;- the notification text handles the limit being turned off between a refused end and its retry.
One question inline (the clock the stretch is measured with). Separately, for the owner rather than for the code: the 15-minute default applies to configs written by earlier builds, so a session on a hot Mac now ends by itself where it did not before, and the end notification is the only place the user sees why — intended, or worth a one-line notice for existing installs on first launch after the upgrade?
| private final class HeatLog { | ||
| var hot = false | ||
| /// When the current serious stretch was first read. | ||
| var seriousSince: Date? |
There was a problem hiding this comment.
The stretch is measured with Date while the wait that triggers the re-read is a Task.sleep, which runs on the continuous clock. The two disagree when the wall clock steps: an NTP correction or a manual time change of, say, +20 minutes between two runs makes seriousFor jump past the limit, and the session ends on a clock change rather than on 15 minutes of heat; a step backwards holds the session longer. RecoveryLock already measures its timeout with ContinuousClock for this reason, and the clock is injected here anyway, so switching seriousSince/seriousFor to ContinuousClock.Instant (or ProcessInfo.systemUptime) keeps the tests as they are and makes both halves of the timer agree.
There was a problem hiding this comment.
Good catch, thanks. Fixed in 92c900e: HeatLog records ContinuousClock.Instant, as RecoveryLock times its timeout, so the stretch and the wait that runs the floors at the limit are on the same clock. The tests now move a FakeContinuousClock apart from the wall clock the manager reads, and a new case jumps the wall clock 20 minutes ahead while 5 minutes pass: the session stays, with 10 minutes to go.
Low Power Mode turned on for a serious thermal state went off at fair, the first step down. That brought back the load that made the Mac serious, so the mode went on and off again as the Mac heated up, with a notification each time. The thermal cause now holds from serious until nominal; FloorRuleDriver keeps that history for the session, in memory. The notification after the cooldown said "Back above the floor.", which is about the battery. It now says "Thermal state is back to nominal.", or "Thermal rules are off." when Settings turned them off, and the one at the start says the mode stays on until then. A battery floor or the thermal rules changed in Settings during a session applied only at the next battery or thermal event. They now run the floors at once, as the lid option already did.
The machine line adds "Thermal: serious" (or fair, critical), read again when the menu opens rather than through the monitor, whose copy moves only with the notification and is not kept current outside a session. The actions line says "Low Power Mode set by Insomnia" while the journal has lowPowerSetByUs, so the reason the Mac is slower is in the menu.
The Settings view asked for a floor run with an onChange per setting, a path no test could reach. SessionManager.config now calls floorInputsChanged when a battery floor, the thermal rules or the lid option changes (Config.floorInputs), and AppServices points it at reevaluateFloors(). The view's onChange calls, the lid option's included, are gone, so there is one path for every floor input. Tests: a floor input change asks for one run and any other change none, and during a session each change applies through that hook with no battery, thermal or lid event, in the power calls, the journal and the session end.
A Mac that stays at fair after serious, under a long build say, keeps Low Power Mode and the held display sample until the session ends. Say so next to the hold, with why a release on a timer is not used: it would bring the cycling back at a slower pace. The menu shows the mode and the state meanwhile, and turning the thermal rules off releases it.
…losed lid Apple silicon throttles before it reaches critical, so a Mac that cannot shed its heat, in a bag say, can stay serious for hours with Low Power Mode on, and the session never ends. A serious stretch that lasts seriousThermalLimit now ends it with the new reason thermalSustained, which is one of the floor-rule ends that put a closed Mac to sleep (krishhgg#61): without that sleep the end would switch Low Power Mode off and unfreeze the apps of a Mac still running in the bag. The limit is 15 minutes by default, also for a config.json without the key, and Settings has "End after serious heat for" in 5-minute steps from Off to 60; a hand-edited value outside that range is clamped at load. The stretch is timed from the first floor run that reads it, and any reading below serious breaks it. The thermal state sends no event while it stays serious, so the driver reports after each run when the stretch reaches the limit and AppServices runs the floors once more then. Once it has, no new wait is owed: a refused end is retried by the manager, not every second.
The stretch was measured with Date while the wait that runs the floors at the limit sleeps on the continuous clock. A step of the wall clock, an NTP correction or a time set by hand, then moved the stretch: 20 minutes ahead ended the session on a clock change, a step back held it longer. HeatLog now records ContinuousClock.Instant, as RecoveryLock times its timeout, and the tests move a FakeContinuousClock apart from the wall clock, with a case where the wall clock jumps and the session stays.
2a1b26c to
92c900e
Compare
|
On the default for existing installs, which @paoValle raised for the owner: it is intended. Serious heat for 15 minutes is a reason to stop holding sleep on any install, and the end notification names the reason ("Thermal state stayed serious for 15 minutes."), with Off in Settings. A one-time notice at the first launch after the upgrade, like the lid-close update's, would be easy to add if you want it; I left it out to keep this small. |
|
Thanks — both of my points are settled then. On the default: the end notification naming the reason, plus Off in Settings, is enough for me. The one-time first-launch notice is a product call for @krishhgg, and I would not widen this PR for it. The clock point is addressed on this head as well: (I read the source, not a build: no Xcode on this machine, so |
Draft: this builds on #61 and #80 and contains their commits until they land. Its own changes are the last two commits, e5cd136 (the end) and 92c900e (the stretch timed on the continuous clock).
Problem
criticalis rare on Apple silicon, which throttles before it gets there. A Mac that cannot shed its heat, in a bag for example (#55, #52), can stay atseriousfor hours with Low Power Mode on, and nothing ends the session.An end alone would make that worse: it switches Low Power Mode off and unfreezes the apps, and macOS does not ask for a closed lid's sleep again (#61), while the computer-use agent's
UserIsActiveevery ~40 s keeps idle sleep away. That is why this waits for #61.Fix
seriousstretch that lastsseriousThermalLimitends the session with a new reason,thermalSustained: "Thermal state stayed serious for 15 minutes. Sleep is back to normal." It is in Lid: a timer, heat or battery end with the lid closed puts the Mac to sleep #61'ssleepsAClosedLid, so with the lid closed the Mac is put to sleep, as after a critical end.Config.floorInputs, so a change in Settings applies at once through Heat: keep Low Power Mode on until nominal, and show it in the menu #80'sfloorInputsChanged.ProcessInfosends no event while the state stays serious, so the driver reports after each run when the stretch reaches the limit (recheck), andAppServicesruns the floors once more at that moment. Once the limit is reached no new wait is owed: an end that the lock or a runningsudo pmsetrefuses is retried bySessionManager, not every second.The end goes through the existing journaled
SessionManager.end, so the journal, the backstop and the scripts do not change.Tests
FloorRulesTests: the end at the limit, with the limit at 0 and with the thermal rules off, and its order against the battery floor, critical and an unreadable battery. Driver cases through therecheckhook: the end at the limit with the journal clean and the notification text, a break that starts the stretch again, the settings changing under a running stretch, an end refused for the lock that owes no new wait, and the text when the limit was turned off before a retry.FloorRuleDriverTests, clock: the wall clock jumps 20 minutes ahead while 5 minutes pass, and the session stays with 10 minutes to go.ClosedLidSleepTests: sustained serious heat with the lid closed ends the session and asks for sleep once, with sleep restored and the lock free.ConfigTests: decoding without the key, the clamp at load, the limit text.swift testat 92c900e, onmaindb9085e with Lid: a timer, heat or battery end with the lid closed puts the Mac to sleep #61 and Heat: keep Low Power Mode on until nominal, and show it in the menu #80, on macOS 27.2 (MacBook Pro M5, Xcode 27.0), with tmux installed: 1,270 tests, 0 skipped. The only failures are the 4 locale assertions listed in Heat: keep Low Power Mode on until nominal, and show it in the menu #80, which fail the same way on unmodifiedmainon this Mac.swift build -c release -Xswiftc -warnings-as-errorspasses.Not verified
docs/releasing.md.