Windows off screen during a screen lock may drop out of Miri#210

opened9d agobySSamanyu Das

Note

🤖 Claude Opus 5.5 responding on behalf of Sam

Draft type: bug (unconfirmed risk). Source: root-cause work on #147 on 1 November 2026.

Problem

While the macOS screen is locked, every AX read from another process fails: each window element answers "invalid element" (-25202), and each app's AX window list is empty. This was observed on Sam's Mac with a lock and no sleep, for all 15 windows in 9 apps, from the lock to the unlock. #147 fixed one engine path that read this as "the window is gone": the 1 s handle-recovery timeout (samanyudas/paneru#104).

Two other engine paths make the same call for windows that are not ordered in, and they do not check the lock:

  • cleanup_unordered_windows (src/ecs/workspace.rs) runs on a timer and closes every strip window that is ordered out and has a dead AX element. While locked, every element looks dead.
  • window_destroyed_trigger (src/ecs/triggers.rs) closes a window on a Space "window destroyed" notice when its element is dead and it is not waiting for a handle. claim_replaced_window_handles only starts that wait for windows that are still ordered in.

Inference, not yet observed: a strip window that is off screen during a lock (on another native Space of its display, for example) is dropped by the first path. A window that the wake moves onto a Space that is not shown is dropped by the second path. Neither path fired in the #147 live runs, where every managed window was on a visible Space.

Acceptance criteria

  • Reproduce or rule out each path live: a managed window on a hidden native Space during a screen lock, and a wake that moves a managed window to a hidden Space.
  • If a path reproduces, the window stays managed through the lock and is still forgotten if it really closed.

Note

🤖 Claude Opus 5.5 responding on behalf of Sam

Triage result: path 1 is ruled out live. Path 2 takes the guarded #147 route by the same measured fact, but its wake case is not observed yet. No fix is needed for anything reproduced so far.

The premise does not hold

The issue assumed that a window on a hidden native Space counts as ordered out. It does not: macOS reports SLSWindowIsOrderedIn = true for it, both unlocked and locked.

  • cleanup_unordered_windows (src/ecs/workspace.rs) closes a window only on window_ordered_in == Ok(false), so a hidden-Space window never qualifies.
  • claim_replaced_window_handles (src/ecs/triggers.rs) gives AwaitingHandle to every ordered-in window with a dead AX element. window_destroyed_trigger then skips it, and recover_window_handles holds it until the unlock, which is the samanyudas/paneru#104 fix.

Live lock test (path 1)

Build: main app with engine a20f56e (the current pin), on Sam's Mac, verify-miri run 20261106-211445.

  1. Opened one AXFixture window (391434) and placed it on display 1's hidden Space 4898 with a Mission Control click. SLSMoveWindowsToManagedSpace on another process's window does nothing, so the click was needed.
  2. Locked the screen with SACLockScreenImmediate at 21:17:43 (ACDT) and sampled engine state and the window server once a second.
  3. Sam unlocked at 21:30:48, and sampling ran 90 s more.

Results:

  • 623 samples locked (13 min), 90 unlocked.
  • Every sample from the corrected sampler shows the same 11 managed windows on the same Spaces (711 identical rows), fixture included.
  • The fixture read ordered_in=true space=[4898] shown=false in every sample.
  • The fixture app's AXWindows read empty during the lock, so the failed-AX condition was present.
  • The engine loop wakes at least every 500 ms (2 s in Low Power Mode), and cleanup_unordered_windows runs on a 1 s timer, so it ran about once a second, roughly 780 times over the 13-minute lock, without dropping the fixture.
  • The engine log has no destroy or close line for the run.
  • verify-miri end restored the session, and its layout check passed for all 10 pre-existing windows.

The first two samples of the run (21:17:44 and 21:17:45) read managed_count=0 from a sampler parsing bug that also read 0 before the lock. They are not a drop.

Path 2: not observed

The wake case needs macOS to move a managed window onto a hidden Space during a real sleep and wake. I did not sleep the Mac, because that would pause every other agent run on it.

Inference, not observed: a window moved to a hidden Space is still ordered in, so it gets AwaitingHandle and waits for the unlock like the windows in the #147 runs. One gap remains in the reasoning. After the unlock, recover_window_handles keeps a window only if its old AX element answers again or adopt_fresh_handle finds a new one in the app's AXWindows. That list leaves out windows on hidden Spaces, so a hidden-Space window whose element the wake replaced would be closed after 1 s. In the #147 runs the old elements revived after the unlock, so this case may never occur.

Next step

Hand check by Sam: with a managed window on a hidden native Space, sleep the Mac (lid close) for about a minute, wake, unlock, and check the window is still managed on its Space. If it holds, both criteria are met and this can close. If it drops, the replaced-element gap above is the first suspect. Labelled needs-info until then.