Skip to content

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

Open
stevehoyek wants to merge 9 commits into
krishhgg:mainfrom
stevehoyek:fix/closed-lid-sleep-after-floor-end
Open

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

Conversation

@stevehoyek

@stevehoyek stevehoyek commented Oct 21, 2026 •

Copy link
Copy Markdown

What changes

This continues #61 by @shikhar1729, who handed it over here. The six commits from #61 are unchanged. On top of them:

  • Current main merged with a merge commit. The only conflict was an import line in TestSupport.swift.
  • The three fixes from the 18 Oct review on Lid: a timer, heat or battery end with the lid closed puts the Mac to sleep #61:
    1. A sleep macOS never grants no longer holds every quit. After 10 refused requests (at most about 5 minutes at the 30 s retry) Insomnia stops asking, logs it, and a quit no longer waits for it. A request put off for a running operation or a sudo pmset left running does not count, and a newly owed sleep starts a new count.
    2. A cleanup retry no longer asks for sleep again. Only an end that finds the session owes the sleep, so a retry of an end that already ran (session.json kept in place) owes nothing new.
    3. A launch that finds session.json expired owes the sleep for that timer end, and asks once reconcile has released the lock.
  • One more, found while reviewing these: a request now waits until a sudo pmset left running has released the lock and its end has begun, not only until it exits.

Fixes #94 while the app runs. As in #61, a session the recovery agent ends after a crash still leaves a closed Mac awake. That needs a change to backstop.sh and is not in this PR.

#81 builds on #61, so it will need to move onto this branch.

How it was tested

  • New tests in ClosedLidSleepTests: testSleepThatIsNeverRestoredIsGivenUpAndTheQuitGoes, testPostponedRequestsDoNotCountTowardTheBound, testANewOwedSleepStartsANewCount, testExpiredSessionAtLaunchWithTheLidClosedAsksForSleep, testExpiredSessionAtLaunchAsksOnceALiveCommandExits, testExpiredSessionAtLaunchAsksWhenTheReconcileRunsAgain and testLaunchReconcileWithTheLidOpenOrNoSessionAsksNothing.
  • testEndThatCannotRemoveTheSessionFileAsksOnceOverItsRetries (was ...StillAsks) now runs over several retries and checks that exactly one sleep request succeeds.
  • I undid each fix in turn and checked that its tests fail.
  • swift test (1,296 tests, 13 skipped for tmux, which this Mac doesn't have) and swift build -c release -Xswiftc -warnings-as-errors pass locally, on Xcode 27.1 and macOS 26.6.2.
  • No new item in docs/releasing.md: the launch path reaches the sleep through the same IOPMSleepSystem call as the existing closed-lid item, and the tests above cover how it gets there.

Real-Mac evidence

I'll post the report from my MacBook as a comment at the final head, once CI and the reviews are done.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the previous lid-opening cancellation issue is fixed.

Summary

Insomnia asks macOS to sleep when a timer or battery or heat rule ends a session with the lid closed.

  • Expired sessions put a closed Mac to sleep after cleanup.

No new actionable issues were found. The previous finding is fixed.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Timer or floor rule ends session] --> B[Watch lid while sleep is owed]
  B --> C[Restore changes and finish queued work]
  B --> D[Lid opening passes debounce]
  D --> E[Cancel owed sleep and stop watcher]
  C --> F{Sleep still owed and lid closed?}
  F -->|Yes| G[Ask macOS to sleep]
  F -->|No| E
  G -->|Granted or refused 10 times| E
  G -->|Refused fewer than 10 times| H[Retry later]
  H --> F
Loading

Reviews (2) · Last reviewed commit: "Lid: an open drops the owed closed-lid s..." · Reviewed by Greptile

shikhar1729 and others added 8 commits October 10, 2026 15:36
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
…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
The only conflict was in the imports: IOKit for FakeClosedLidSleep on this
branch, Security from krishhgg#100 on main. Both stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t at launch

The three fixes from the 18 Oct review of krishhgg#61:

- A sleep macOS never grants no longer holds every quit. After 10 refused
  requests (at most about 5 minutes at the 30 s retry) the owed sleep is
  given up and logged, and a quit no longer waits for it. A postponed
  request does not count, and a newly owed sleep starts a new count.
  AppDelegate asks SessionManager.letsQuitGo(after:).
- A cleanup retry no longer asks again. Only an end that finds the session
  owes the sleep, so a retry of an end that has run (session.json kept in
  place) owes nothing new.
- A launch that finds session.json expired owes the sleep for that timer
  end, and asks once the reconcile has released the lock.

A request also waits until a sudo pmset left running has released the
lock and its end has begun, not only until it exits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
From Greptile's review of krishhgg#138: sleepClosedLidIfOwed saw an open lid only
when an ask ran, and the end had already stopped the session's lid
observer. A lid opened and closed again between two asks, or while the end
waited for a sudo pmset left running, went unseen, and the next ask put the
Mac to sleep.

SessionManager now watches the lid with a LidObserver of its own from the
end that owes the sleep until the sleep is dropped. An open that holds for
the 2 s debounce drops it, and closing the lid again does not bring it
back. Every place that dropped the sleep goes through dropClosedLidSleep,
which also cancels the next ask and stops the watcher.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@stevehoyek

Copy link
Copy Markdown
Author

Real-Mac report for 98d0b74

Build. A clean checkout of 98d0b74, installed with ./scripts/install.sh on 24 Oct.

$ git rev-parse HEAD
98d0b747567e6c9780aa7f55232340bd1977a319
$ git status --short
$

Mac. No external display.

ProductName:		macOS
ProductVersion:		26.6.2
BuildVersion:		25G83
Mac17,2
Apple M5

Times in insomnia.log are UTC. Times in pmset output are local (+03:00 on 24 Oct, +02:00 on 25 Oct).

A timer or floor-rule end with the lid closed puts the Mac to sleep: pass

Three runs, all ending with the lid closed. Each time the log says session end with the lid closed: sleep requested and macOS slept within 6 s on Software Sleep pid=20373, which is Insomnia.

  • Timer, on AC (24 Oct). 3-minute session. End at 08:25:46Z, slept at 11:25:52, woke when I opened the lid at 11:26:08. The sampler shows SleepDisabled 1 through the session and 0 from 11:25:49.
  • Timer, on battery at 14% (25 Oct). End at 19:55:19Z, slept at 21:55:24 and stayed asleep until I opened the lid at 22:04:10. Idle sleep was held off at the time (PrevIdle in the assertion summary), which is the Mac stays awake indefinitely when a session ends with the lid closed #94 case.
  • Battery end floor at 10% (25 Oct). End (batteryFloor) at 20:48:34Z at 9%, slept at 22:48:40, with coreaudiod holding PreventUserIdleSystemSleep until 22:48:35, after the request. It stayed asleep, apart from maintenance dark wakes, until I opened the lid at 22:59:41.

The sampler was not running for the two battery runs. The pmset -g log lines below show the sleep for each.

Also seen: a session started at 8%, under the floor, with the lid open, ended at once and asked for no sleep.

The same end with its disablesleep 0 left running, lid opened and closed again: not run

It needs a sudo pmset that is still running when the session ends, which I didn't set up on this Mac. testOpeningAndClosingTheLidBetweenAsksDropsTheOwedSleep and the other lid-open tests in ClosedLidSleepTests cover the logic.

insomnia.log
# 24 Oct, timer, AC
2026-10-24T08:22:50Z [info] insomnia: session started until 2026-10-24T08:25:45Z (180s requested)
2026-10-24T08:22:50Z [info] insomnia: lid observer started (lid closed)
2026-10-24T08:22:51Z [info] insomnia: low power mode on
2026-10-24T08:22:51Z [info] insomnia: low power mode on (lid closed)
2026-10-24T08:25:30Z [info] insomnia: lid open
2026-10-24T08:25:30Z [info] insomnia: low power mode off (lid opened)
2026-10-24T08:25:38Z [info] insomnia: lid closed
2026-10-24T08:25:38Z [info] insomnia: display sleep requested
2026-10-24T08:25:38Z [info] insomnia: low power mode on
2026-10-24T08:25:38Z [info] insomnia: low power mode on (lid closed)
2026-10-24T08:25:46Z [info] insomnia: lid observer started (lid closed)
2026-10-24T08:25:46Z [info] insomnia: session end (timer)
2026-10-24T08:25:46Z [info] insomnia: sleep restored
2026-10-24T08:25:46Z [info] insomnia: low power mode cleared
2026-10-24T08:25:46Z [info] insomnia: session end with the lid closed: sleep requested
# 25 Oct, timer, battery
2026-10-25T19:54:20Z [info] insomnia: battery 14% on battery
2026-10-25T19:55:19Z [info] insomnia: lid observer started (lid closed)
2026-10-25T19:55:19Z [info] insomnia: session end (timer)
2026-10-25T19:55:19Z [info] insomnia: sleep restored
2026-10-25T19:55:19Z [info] insomnia: low power mode cleared
2026-10-25T19:55:19Z [info] insomnia: session end with the lid closed: sleep requested
# 25 Oct, battery end floor
2026-10-25T20:04:31Z [info] insomnia: session started until 2026-10-25T21:04:26Z (3600s requested)
2026-10-25T20:04:31Z [info] insomnia: lid observer started (lid open)
2026-10-25T20:04:31Z [info] insomnia: power monitor started (battery 14%, on battery, thermal nominal)
2026-10-25T20:04:31Z [info] insomnia: low power mode on
2026-10-25T20:05:05Z [info] insomnia: lid closed
2026-10-25T20:05:05Z [info] insomnia: display sleep requested
2026-10-25T20:12:14Z [info] insomnia: lid open
2026-10-25T20:35:29Z [info] insomnia: battery 10% on battery
2026-10-25T20:38:01Z [info] insomnia: session extended by 3600s until 2026-10-25T22:04:26Z
2026-10-25T20:38:12Z [info] insomnia: lid closed
2026-10-25T20:38:12Z [info] insomnia: display sleep requested
2026-10-25T20:48:34Z [info] insomnia: battery 9% on battery
2026-10-25T20:48:34Z [info] insomnia: lid observer started (lid closed)
2026-10-25T20:48:34Z [info] insomnia: session end (batteryFloor)
2026-10-25T20:48:34Z [info] insomnia: sleep restored
2026-10-25T20:48:34Z [info] insomnia: low power mode cleared
2026-10-25T20:48:34Z [info] insomnia: notify: Session ended - Battery fell below 10%. Sleep is back to normal.
2026-10-25T20:48:34Z [info] insomnia: session end with the lid closed: sleep requested
# 25 Oct, started below the floor
2026-10-25T21:00:20Z [info] insomnia: session started until 2026-10-25T22:00:14Z (3600s requested)
2026-10-25T21:00:20Z [info] insomnia: lid observer started (lid open)
2026-10-25T21:00:20Z [info] insomnia: power monitor started (battery 8%, on battery, thermal nominal)
2026-10-25T21:00:20Z [info] insomnia: lid observer started (lid open)
2026-10-25T21:00:20Z [info] insomnia: session end (batteryFloor)
2026-10-25T21:00:20Z [info] insomnia: sleep restored
2026-10-25T21:00:20Z [info] insomnia: notify: Session ended - Battery fell below 10%. Sleep is back to normal.
2026-10-25T21:00:20Z [info] insomnia: session end: the lid is open, or macOS keeps this closed Mac awake, so no sleep is requested
pmset samples (24 Oct)
11:22:28 SleepDisabled 0 lowpowermode 0
11:22:33 SleepDisabled 0 lowpowermode 0
11:22:38 SleepDisabled 0 lowpowermode 0
11:22:43 SleepDisabled 0 lowpowermode 0
11:22:48 SleepDisabled 1 lowpowermode 0
11:22:53 SleepDisabled 1 lowpowermode 1
11:22:58 SleepDisabled 1 lowpowermode 1
11:23:03 SleepDisabled 1 lowpowermode 1
11:23:08 SleepDisabled 1 lowpowermode 1
11:23:13 SleepDisabled 1 lowpowermode 1
11:23:18 SleepDisabled 1 lowpowermode 1
11:23:23 SleepDisabled 1 lowpowermode 1
11:23:28 SleepDisabled 1 lowpowermode 1
11:23:33 SleepDisabled 1 lowpowermode 1
11:23:38 SleepDisabled 1 lowpowermode 1
11:23:43 SleepDisabled 1 lowpowermode 1
11:23:48 SleepDisabled 1 lowpowermode 1
11:23:53 SleepDisabled 1 lowpowermode 1
11:23:58 SleepDisabled 1 lowpowermode 1
11:24:03 SleepDisabled 1 lowpowermode 1
11:24:08 SleepDisabled 1 lowpowermode 1
11:24:14 SleepDisabled 1 lowpowermode 1
11:24:19 SleepDisabled 1 lowpowermode 1
11:24:24 SleepDisabled 1 lowpowermode 1
11:24:29 SleepDisabled 1 lowpowermode 1
11:24:34 SleepDisabled 1 lowpowermode 1
11:24:39 SleepDisabled 1 lowpowermode 1
11:24:44 SleepDisabled 1 lowpowermode 1
11:24:49 SleepDisabled 1 lowpowermode 1
11:24:54 SleepDisabled 1 lowpowermode 1
11:24:59 SleepDisabled 1 lowpowermode 1
11:25:04 SleepDisabled 1 lowpowermode 1
11:25:09 SleepDisabled 1 lowpowermode 1
11:25:14 SleepDisabled 1 lowpowermode 1
11:25:19 SleepDisabled 1 lowpowermode 1
11:25:24 SleepDisabled 1 lowpowermode 1
11:25:29 SleepDisabled 1 lowpowermode 1
11:25:34 SleepDisabled 1 lowpowermode 0
11:25:39 SleepDisabled 1 lowpowermode 1
11:25:44 SleepDisabled 1 lowpowermode 1
11:25:49 SleepDisabled 0 lowpowermode 0
11:26:09 SleepDisabled 0 lowpowermode 0
11:26:14 SleepDisabled 0 lowpowermode 0
11:26:19 SleepDisabled 0 lowpowermode 0
11:26:24 SleepDisabled 0 lowpowermode 0
11:26:29 SleepDisabled 0 lowpowermode 0
pmset -g log
2026-10-24 11:25:52 +0300 Sleep               	Entering Sleep state due to 'Software Sleep pid=20373':TCPKeepAlive=active Using AC (Charge:100%) 16 secs
2026-10-24 11:26:08 +0300 Wake                	Wake from Deep Idle [CDNVA] : due to smc.sysState.Wake(0x70070000) lid SMC.OutboxNotEmpty nub-spmi-a0.0x3F RTP.multi-touch/UserActivity Assertion Using AC (Charge:100%)
2026-10-25 21:55:21 +0200 Assertions          	Summary- [System: PrevIdle DeclUser SRPrevSleep kCPU kDisp] Using Batt(Charge: 14)
2026-10-25 21:55:24 +0200 Assertions          	Summary- [System: PrevIdle DeclUser SysAct kDisp] Using Batt(Charge: 14)
2026-10-25 21:55:24 +0200 Sleep               	Entering Sleep state due to 'Software Sleep pid=20373':TCPKeepAlive=active Using Batt (Charge:14%) 526 secs
2026-10-25 22:04:10 +0200 Wake                	Wake from Deep Idle [CDNVA] : due to smc.sysState.Wake(0x70070000) lid SMC.OutboxNotEmpty RTP.multi-touch/HID Activity Using BATT (Charge:14%)
2026-10-25 22:48:35 +0200 Assertions          	PID 416(coreaudiod) Released PreventUserIdleSystemSleep "com.apple.audio.context18674.preventuseridlesleep" 00:44:23  id:0x0x100008bc6 [System: PrevIdle DeclUser SRPrevSleep IPushSrvc kCPU kDisp]
2026-10-25 22:48:40 +0200 Sleep               	Entering Sleep state due to 'Software Sleep pid=20373':TCPKeepAlive=active Using Batt (Charge:9%) 141 secs
2026-10-25 22:51:01 +0200 DarkWake            	DarkWake from Deep Idle [CDNP] : due to smc.sysState.Wake(0x70070000) wifibt SMC.OutboxNotEmpty/ Using BATT (Charge:9%) 45 secs
2026-10-25 22:51:46 +0200 Sleep               	Entering Sleep state due to 'Maintenance Sleep':TCPKeepAlive=active Using Batt (Charge:9%) 6 secs
2026-10-25 22:51:52 +0200 DarkWake            	DarkWake from Deep Idle [CDNP] : due to smc.sysState.Wake(0x70070000) wifibt SMC.OutboxNotEmpty bluetooth-pcie/ Using BATT (Charge:9%) 45 secs
2026-10-25 22:52:37 +0200 Sleep               	Entering Sleep state due to 'Maintenance Sleep':TCPKeepAlive=active Using Batt (Charge:9%) 77 secs
2026-10-25 22:53:54 +0200 DarkWake            	DarkWake from Deep Idle [CDNP] : due to smc.sysState.Wake(0x70070000) wifibt SMC.OutboxNotEmpty/ Using BATT (Charge:9%) 45 secs
2026-10-25 22:54:39 +0200 Sleep               	Entering Sleep state due to 'Maintenance Sleep':TCPKeepAlive=active Using Batt (Charge:9%) 120 secs
2026-10-25 22:56:39 +0200 DarkWake            	DarkWake from Deep Idle [CDNP] : due to smc.sysState.Wake(0x70070000) wifibt SMC.OutboxNotEmpty/ Using BATT (Charge:9%) 45 secs
2026-10-25 22:57:24 +0200 Sleep               	Entering Sleep state due to 'Maintenance Sleep':TCPKeepAlive=active Using Batt (Charge:9%) 137 secs
2026-10-25 22:59:41 +0200 Wake                	Wake from Deep Idle [CDNVA] : due to smc.sysState.Wake(0x70070000) lid SMC.OutboxNotEmpty RTP.multi-touch/HID Activity Using BATT (Charge:9%) 49 secs

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.

Mac stays awake indefinitely when a session ends with the lid closed

3 participants