Skip to content

Lid: a timer, heat or battery end with the lid closed puts the Mac to sleep - #61

Closed
shikhar1729 wants to merge 6 commits into
krishhgg:mainfrom
shikhar1729:fix/closed-lid-sleep-after-floor-end
Closed

shikhar1729 wants to merge 6 commits into
krishhgg:mainfrom
shikhar1729:fix/closed-lid-sleep-after-floor-end

Conversation

@shikhar1729

@shikhar1729 shikhar1729 commented Oct 10, 2026 •

Copy link
Copy Markdown

Problem

macOS asks for a closed lid's sleep once, when the lid closes. During a session, disablesleep 1 refuses it. disablesleep 0 does not make macOS ask again (xnu IOPMrootDomain.cpp, kPowerEventUserDisabledSleep). So a Mac whose session ends with the lid closed keeps running until the lid opens, or until idle sleep, which another app's assertion can hold off for hours (#94).

Fix

After a timer or floor-rule end (timer, thermalCritical, batteryFloor, batteryUnreadable), if the lid is closed and AppleClamshellCausesSleep is true, Insomnia calls IOPMSleepSystem.

  • The sleep stays owed until it is asked. It is asked only with no session, no lifecycle operation queued or running, and no live sudo pmset.
  • If macOS refuses (sleep still disabled after a failed restore), it asks again every 30 s.
  • A start that takes the lock, an open lid, or an external display in clamshell mode drops it.
  • Only the app asks, so a quit waits while the sleep is owed.
  • No new process and no new sudo rule.
  • User, quit and backstop ends owe no sleep.

Tests

  • 17 new tests in ClosedLidSleepTests, including a failed restore, a quit that fails again, a session.json that cannot be removed, a quit during an end, and a start queued behind or refused during one.
  • I have no Xcode, so I ran only these 17, with a local XCTest stand-in. All pass. CI runs the full suite.
  • swift build -c release -Xswiftc -warnings-as-errors passes.
  • Not tried on hardware yet. I added it to the real-Mac checks in docs/releasing.md.

https://claude.ai/code/session_01Jco1ESE6NxipTXFsZE31rZ

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the previous quit issue is fixed and no new actionable issue was found.

Summary

Insomnia requests sleep when a timer, battery rule, or heat rule ends a session with the lid closed, unless macOS keeps the closed Mac awake.

  • Timer and floor-rule ends can put a closed Mac to sleep.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Timer or floor rule ends session] --> B[Restore saved changes]
  B --> C{Sleep still owed?}
  C -->|No| D[Done]
  C -->|Yes| E{Session or other work active?}
  E -->|Yes| F[Check again in 30 seconds]
  E -->|No| G{Closed lid should sleep?}
  G -->|No| H[Drop owed sleep]
  G -->|Yes| I[Ask macOS to sleep]
  I -->|Refused| F
  I -->|Accepted| H
  F --> C
Loading

Reviews (7) · Last reviewed commit: "Lid: a timer end with the lid closed put..." · Reviewed by Greptile

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
aldikosh23 added a commit to aldikosh23/Insomnia that referenced this pull request Oct 11, 2026
aldikosh23 added a commit to aldikosh23/Insomnia that referenced this pull request Oct 11, 2026
…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.
aldikosh23 added a commit to aldikosh23/Insomnia that referenced this pull request Oct 12, 2026
aldikosh23 added a commit to aldikosh23/Insomnia that referenced this pull request Oct 12, 2026
…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.
@aldikosh23

Copy link
Copy Markdown
Contributor

#94 reports the same thing after a timer end: the session ended on its timer with the lid closed, disablesleep 0 went through, and the Mac stayed awake for about five hours until the lid opened, with idle sleep held off by another app's assertion (75% to 25% battery). sleepsAClosedLid leaves .timer out here. With the same checks (the lid closed, AppleClamshellCausesSleep, nothing queued), would it belong in the list too? A Mac used closed on an external display reads AppleClamshellCausesSleep false, so it would still be left awake.

For context, #81 (a draft on top of this one) adds .thermalSustained, an end after 15 minutes of serious heat, to the same list.

…uit waits for it

A timer end with the lid closed left the Mac awake for hours when
another app held off idle sleep (krishhgg#94). A quit whose disablesleep 0
failed again exited and dropped the owed sleep, which only the app
asks for.

Claude-Session: https://claude.ai/code/session_01Jco1ESE6NxipTXFsZE31rZ
@shikhar1729 shikhar1729 changed the title Lid: a heat or battery end with the lid closed puts the Mac to sleep Lid: a timer, heat or battery end with the lid closed puts the Mac to sleep Oct 12, 2026
@shikhar1729

Copy link
Copy Markdown
Author

@aldikosh23 Agreed, thanks. d0b7a1c adds .timer to sleepsAClosedLid, with the same checks, so an external display in clamshell mode still keeps the Mac awake. Test: testTimerAndBatteryEndsWithTheLidClosedAskForSleep.

This covers #94 while the app runs. A session that the recovery agent ends after a crash still has the gap. That needs a change to backstop.sh, so I left it out of this PR.

SergeiNikolenko added a commit to SergeiNikolenko/Insomnia that referenced this pull request Oct 16, 2026
@krishhgg
krishhgg marked this pull request as draft October 17, 2026 02:51
@krishhgg
krishhgg marked this pull request as ready for review October 17, 2026 02:51
@krishhgg

krishhgg commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Thanks for this. Three things before it can merge: merge main, fix three cases where the owed sleep goes wrong, and then a test report from your MacBook.

1. Merge main

The branch now conflicts with main in Tests/InsomniaTests/TestSupport.swift, after #100 merged. Please merge current main into this branch with a merge commit (no rebase or force-push), resolve the conflict, and push.

2. Three fixes

A sleep that is never granted blocks quitting forever. Say the sudoers rule goes away during a session. The end can't restore sleep, so every sleep request fails, even with the recovery agent loaded. With the lid closed, closedLidSleepOwed then stays true, and InsomniaApp.swift:107 cancels every quit, with no time limit and no way to quit anyway. Fix: hand the owed sleep to the recovery path that outlives the app, or give up after a bounded time and let a quit go through. Add a test where sleep is never restored.

Cleanup retries ask for sleep again. SessionManager.swift:1340 sets closedLidSleepOwed on every end call. When session.json can't be removed, the first sleep request works and clears the flag. Then scheduleEndRetry calls end again with the same timer or floor reason, which sets the flag again. A closed Mac that was woken for remote access is sent back to sleep on each retry. Fix: remember that this end already asked for sleep. Extend the retained-file test over several retries, and check that exactly one sleep request succeeds.

Launch recovery skips the sleep for an expired timer. performReconcile() calls performEnd(reason: .timer) directly (line 2751), so it skips the two steps end takes: setting the owed flag (line 1340) and asking for sleep (line 1377). Relaunching after the timer ran out therefore restores sleep but leaves a closed Mac awake when another app holds off idle sleep, which is the #94 case. Fix: record the owed sleep for this timer end too, and act on it after reconcile releases its lock. Add a test for a closed lid with an expired session at launch.

3. A test report from your MacBook

CI covers the logic. This report covers a real lid close, which needs a MacBook. Run it after the fixes, with no external display connected.

  1. A timer end with the lid closed puts the Mac to sleep, even when another app keeps it awake. In Terminal, run caffeinate -i and leave it running. It holds off idle sleep the way the app in Mac stays awake indefinitely when a session ends with the lid closed #94 did. Start a 3-minute session, close the lid within the first minute, and open it about 3 minutes after the session ended. Expected: insomnia.log has session end (timer) and then session end with the lid closed: sleep requested, and pmset -g log shows a Sleep entry a few seconds later.
  2. A battery end floor does the same. On battery, set "End session below" in Settings a little under the current charge, start a session, and close the lid. Expected: when the charge drops past the floor, the log has session end (batteryFloor) and the sleep line, and pmset -g log shows a Sleep entry right after. This one waits on the battery, so it can take a while.

For each check, post the insomnia.log lines for that run and pmset -g log | grep -E ' (Sleep|Wake|DarkWake) ' for that time window. If you have an external display, a third run of check 1 in clamshell mode should keep the Mac awake.

Build and what to post

  • Build. This branch at the head after the fixes, with no local changes, installed with ./scripts/install.sh. Building the app needs Xcode, since the Command Line Tools lack the SwiftUI macro plugins. If installing Xcode isn't an option for you, say so here. Post git rev-parse HEAD and the output of git status --short, which should be empty.
  • Mac. Post sw_vers, sysctl -n hw.model and sysctl -n machdep.cpu.brand_string.
  • For each check, post the steps you ran with times, pass or fail, and the raw output named in the check.
  • Not run. List anything you couldn't run, and why.

Post the results as a comment on this PR. Put long logs in a gist and link it. If you push another commit, the report needs to be redone at the new head.

@stevehoyek

Copy link
Copy Markdown

@shikhar1729 thanks for this fix. #94 is what's stopping me from using Insomnia, so I'd like to see this merged. Are you planning to make the three changes from the 18 Oct review? If you're short on time, I'm happy to take them on in a branch built on your commits, with credit to you, and run the real-Mac checks on my MacBook.

@shikhar1729

Copy link
Copy Markdown
Author

Go ahead, take it over. Thanks!

@stevehoyek

Copy link
Copy Markdown

@shikhar1729 thanks again for this. I've opened #138 from your branch: your commits are as they were, with main merged and the three fixes from the review on top. I'll post the MacBook report there.

@shikhar1729 shikhar1729 closed this Nov 1, 2026
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.

4 participants