Skip to content

fix(macos): recover device io after missed wake notifications - #1395

Open
minatoaquaMK2 wants to merge 1 commit into
AprilNEA:masterfrom
minatoaquaMK2:fix/macos-device-io-resume
Open

minatoaquaMK2 wants to merge 1 commit into
AprilNEA:masterfrom
minatoaquaMK2:fix/macos-device-io-resume

Conversation

@minatoaquaMK2

@minatoaquaMK2 minatoaquaMK2 commented Sep 15, 2026

Copy link
Copy Markdown

Summary

On macOS with OpenLogi 0.8.3, a user repeatedly found that a Bluetooth M720 was no longer recognized after turning the mouse off, putting the Mac to sleep, and waking it again. Rebooting the Mac and reopening OpenLogi restored detection. This change addresses a lifecycle defect that can leave discovery suspended indefinitely after such a cycle.

Evidence from the reported failure

The agent's persisted log and macOS unified log were compared for September 15, 2026. Times below are UTC+08:00; the agent log itself records UTC.

Time Observation
08:34:43 The agent logged display/session suspended — pausing device I/O.
18:11:59 macOS recorded a transition from DarkWake to FullWake due to HID activity, followed by display Did Wake.
18:12:09 macOS recorded completion of screen unlock. The agent had no corresponding device-I/O resume entry.
18:55–18:56 The old agent still emitted IPC transport warnings, but had not logged device-I/O recovery.
19:26:50 The Mac rebooted, as confirmed by its boot time.
19:35:18–19:35:20 The newly started agent enabled device I/O and activated mouse control capture. A subsequent agent-backed inventory query returned the Bluetooth M720.

This establishes that the machine had visibly resumed while the agent's last logged I/O state remained suspended. It does not identify the exact missing or out-of-order AppKit callback: the old logging only recorded changes to the overall gate, not each suspension source. Reboot recovery alone would also be insufficient to identify a notification bug. The code and regression tests establish the indefinite-suspension failure mode addressed here. The hardware follow-up below records successful detection after sleep, but also exposes a display-power reconciliation issue that remains unresolved.

Recurrence observed on September 17

The same failure recurred on the unchanged installed 0.8.3 agent, which had been running since September 15. At 08:22:33 UTC+08:00, macOS logged a display-on event and full wake. By 08:24, Bluetooth reported the M720 connected and IORegistry exposed its BLE HID device, but the agent-backed inventory was empty. Its last lifecycle entry remained the previous night's I/O suspension, with no subsequent resume entry.

A live stack sample showed the main thread normally waiting in the AppKit run loop and the inventory watcher waiting in its event loop; the stripped binary did not expose the suspension mask or exact awaited future. At 08:25:20, restarting only the same signed agent through LaunchServices restored discovery and control capture within seconds. No reboot, Bluetooth reset, permission change, or binary replacement was involved.

This narrows the reproduction beyond the initial reboot-based observation and supports the existing lifecycle-reconciliation approach. It is still evidence from the old agent, not hardware validation of this PR. The exact missing or late callback remains unproven.

Why discovery cannot recover on its own

The DarkWake protection introduced in #1142 intentionally closes the HID gate during system sleep, display sleep, or an inactive user session. This prevents BLE HID access during maintenance wakes from promoting them into visible wakes. That protection must remain in place.

In openlogi-agent/src/tray.rs, the lifecycle observer accumulates SYSTEM_SLEEP, SCREEN_SLEEP, and SESSION_INACTIVE flags. A screen-wake callback clears the system/display flags; a session-active callback clears the system/session flags. Device I/O resumes only when all suspension flags are clear. Previously, after startup there was no independent check of the actual display or console-session state. A missing wake callback or a late sleep callback could therefore leave a stale flag set with no further event to clear it.

In openlogi-agent-core/src/watchers/inventory.rs, both the inventory loop and its suspended-state select branch await wait_until_allowed(). The ordinary 30-second recovery scan is behind that same gate. Consequently, the gate's stale state also disables the mechanism that would normally recover missed device events. Reconnecting the mouse cannot make that scan run, and simply increasing probe retries would not address the owning lifecycle state.

How this change restores progress

The macOS lifecycle owner now performs a read-only reconciliation every five seconds using a run-loop timer. It checks that the main display is active and awake, then reads CGSessionCopyCurrentDictionary to confirm that the process's session is on the console. Only that positive snapshot clears stale system/display/session suspension flags. The startup flag is preserved.

An asleep or inactive display, an off-console session, a missing session dictionary, or an unavailable/unexpected console-state value does not authorize recovery. Generic system-wake notifications still do not open the gate. The checks themselves neither open HID devices nor assert user activity. They are intended to preserve DarkWake protection, but the hardware follow-up below shows that the current snapshot can authorize the gate while the system reports the display off; that issue is not yet resolved.

The timer lives in the lifecycle owner, outside the gated inventory loop. Once it reopens the gate, the existing resume path performs settled inventory reconciliation and settings replay. Source/previous/remaining suspension masks and stale-state recovery are now logged so a future report can distinguish these transitions. Five seconds is the timer interval, not a hard latency guarantee: run-loop scheduling and subsequent device probing can add delay.

September 17 hardware follow-up: detection recovered; DarkWake issue remains

The reporter confirmed normal use after opening the lid around 18:36, with no recurrence of the missing-mouse symptom. The installed agent hash still matches 0becae2b, and the same process remained alive from 08:48:49 through the evening check. There was no agent restart during the sleep/wake sequence.

Time (UTC+08:00) Observation
08:58:13–08:58:18 The agent suspended I/O with system + screen mask 3; macOS entered clamshell sleep.
Until 18:27 27 maintenance DarkWakes had no recorded display-on event or agent I/O resume.
18:27:42–18:27:43 A short full wake occurred with the lid closed. Bluetooth logs map the wake's LE-data peripheral identifier to the M720. The native screen-wake callback then cleared mask 3.
18:27:52 → 18:27:56 macOS recorded display-off and the agent set system-sleep mask 1. About four seconds later, the new timer cleared it and logged stale-state recovery, while the system still reported the display off.
18:28:11–18:28:18 A network-packet DarkWake was followed by M720 HID opens attributed to openlogi-agent, then promotion to full wake due to Bluetooth LE HID activity.
18:28:28 → 18:28:32 The display-off/system-sleep sequence repeated, and the timer again cleared mask 1. Probe retries continued during the subsequent maintenance DarkWake.
18:36:24 → 18:36:30 Actual lid-open full wake was followed by active M720 control capture in about six seconds. The 19:03 agent-backed inventory query returned the connected M720.

This confirms a successful user-visible recovery without restarting the agent and execution of the timer path. It does not establish a clean stale-callback recovery: the two timer recoveries occurred after system display-off events. The current CoreGraphics/session snapshot disagreed with the system power timeline, and the reopened gate allowed actual HID opens during DarkWake. DarkWake protection is not validated; this premature-reopening issue needs to be addressed before considering the fix complete.

For wake attribution, the first full wake is tied to M720 Bluetooth data, without enough detail to distinguish a physical click/movement from another device report. The second started as a network DarkWake, then became a Bluetooth HID full wake while agent probing was active. The trace does not prove that OpenLogi caused the full-wake promotion or that an iPhone caused the initial network wake. Nearby iPhone advertisements alone are not causal evidence. Device UUIDs, addresses, and serial numbers are omitted here.

September 18 recurrence: agent exited; local supervisor registration was stale

At 08:24 after lid-open wake, the reporter again saw no mouse in OpenLogi. This time no agent process was running: the CLI explicitly fell back to direct HID access and found the connected M720. The fixed agent's last log was from 21:49:40 the previous night: its independent CGEventTap lifecycle watchdog exited after a 1,590 ms progress gap in phase Armed. macOS recorded a maintenance DarkWake at that time, and RunningBoard confirmed process termination. The log does not distinguish an actual stuck tap operation from a resume scheduling delay; it does not implicate the discovery gate or prove that this PR's timer caused the exit.

Automatic recovery was also broken by the local test installation. The launchd registration still referenced the production application's old build/signing requirement and repeatedly failed to resolve/execute its helper. The local build kept package version 0.8.3, so the GUI's package-version registration marker did not detect the changed local binary/signature. This installation validation step had been missed.

Starting the same installed helper restored M720 capture in seconds. Forcing the GUI's existing SMAppService re-registration path updated the registered build and signing requirement. With the GUI closed, the registered agent was then deliberately killed once: launchd automatically started a new PID, and both the OS hook and agent-backed M720 capture/inventory recovered. No rebuild or permission changes were needed. This repairs local automatic recovery; the watchdog's wake-time timeout and the previously observed premature DarkWake gate reopening remain unresolved.

Changes

  • openlogi-agent: add read-only lifecycle reconciliation, suspension-state diagnostics, and regression tests for missing/late notifications and recovery exclusions.
  • openlogi-agent: enable the macOS-only CoreFoundation, CGSession, and NSTimer bindings required by the snapshot and timer.
  • Documentation: update the macOS FFI inventory. The lockfile only adds the agent's existing CoreFoundation dependency; GPUI versions are unchanged by this PR.

Testing

Post-rebase commands used RUSTUP_TOOLCHAIN=1.98.0, RUSTFLAGS="-D warnings", and DEVELOPER_DIR=/Library/Developer/CommandLineTools.

  • Passed: cargo fmt --all -- --check.

  • Passed: cargo test -p openlogi-agent (45 tests).

  • Passed: cargo clippy -p openlogi-agent --all-targets -- -D warnings.

  • Passed: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items --exclude openlogi-ui --exclude openlogi-desktop --exclude openlogi-overlay --exclude openlogi-agent.

  • Blocked: cargo clippy --workspace --all-targets -- -D warnings; gpui-pre-apple cannot find xcrun metal because this host has Command Line Tools but no full Xcode/Metal toolchain.

  • Skipped: cargo xtask ci cargo-deny; the runner reports that neither cargo-deny nor Nix is installed. This is not a pass.

  • Not run: full-workspace tests, which require the same missing Metal compiler; Linux/Windows cross-lints (only the macOS target is installed). The macOS-only source and dependency changes were manually audited against the refreshed upstream master.

  • Before rebase, disabling the new lifecycle reconciliation made the missing-notification regression test fail; restoring it made the test pass.

  • Local installation build: agent, overlay, and CLI built with cargo build --locked --release -p openlogi-desktop --bin openlogi-desktop -p openlogi-agent --bin openlogi-agent -p openlogi-overlay --bin openlogi-overlay -p openlogi --bin openlogi --features gpui_platform/runtime_shaders. The desktop target stopped on existing upstream unused imports (IntoElement, Palette); it subsequently built with SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk cargo rustc --locked --release -p openlogi-desktop --bin openlogi-desktop --features gpui_platform/runtime_shaders -- --cap-lints warn. Runtime shaders avoid the missing build-time Metal compiler for this installation. This is not a strict full-workspace gate pass.

  • Hardware smoke test on September 17: installed all four binaries from 0becae2b as a locally ad-hoc-signed bundle on arm64 macOS 27.0. After reauthorizing Input Monitoring and Accessibility for the new signature, the installed CLI read the running agent's inventory and returned the connected Bluetooth M720. Agent logs confirmed active control capture and installation of the OS input hook; the GUI rendered successfully. Installed hashes and signatures were verified.

  • Hardware sleep/wake follow-up: the reporter saw no missing-mouse recurrence; same-process recovery and M720 capture after the 18:36 lid-open wake are confirmed by logs and the agent-backed inventory. Exact physical mouse-power timing is not established.

  • DarkWake validation is not passed: logs show timer-driven gate reopening after display-off, followed by HID opens during DarkWake. See the timestamped hardware section above. Further work must verify both reliable visible-wake recovery and continued suspension during display-off/maintenance wakes.

  • Follow-up evidence was read with pmset -g log, log show --style compact --info --debug --start '2026-09-17 18:27:35' --end '2026-09-17 18:28:25' using Bluetooth/HID process predicates, process start-time inspection, installed-binary SHA-256 comparison, and /Applications/OpenLogi.app/Contents/MacOS/openlogi list. No code changes, rebuilds, agent restarts, or permission changes were made during this follow-up.

  • September 18 local supervision check passed: launchctl print gui/501/org.openlogi.agent.service showed the repaired registration and a running agent; after launchctl kill SIGKILL gui/501/org.openlogi.agent.service with the GUI closed, launchd automatically created a new PID and /Applications/OpenLogi.app/Contents/MacOS/openlogi list again read M720 from that agent. This tests abnormal-exit recovery, not the cause of the watchdog timeout.

@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

No new actionable issue was introduced by the changes made since the previous review.

Summary

Adds macOS lifecycle reconciliation so missed or out-of-order wake notifications do not leave device I/O suspended indefinitely.

  • Samples display and console-session state every five seconds from the AppKit run loop.
  • Clears stale lifecycle suspension flags only for a visible console session while preserving the startup gate.
  • Adds lifecycle transition diagnostics and regression coverage for recovery and exclusion states.
  • Enables the required CoreFoundation, CoreGraphics session, and Foundation timer bindings and updates the FFI inventory.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[macOS lifecycle notifications] --> B[Update suspension-source mask]
    B --> C{Mask clear?}
    C -- Yes --> D[Enable device I/O]
    C -- No --> E[Keep device I/O suspended]
    F[Five-second NSTimer] --> G[Read display and console-session snapshot]
    G --> H{Visible console session?}
    H -- No or unavailable --> E
    H -- Yes --> I[Clear stale system, screen, and session flags]
    I --> C
Loading

Reviews (2) · Last reviewed commit: "fix(macos): reconcile device io after mi..."

@minatoaquaMK2
minatoaquaMK2 force-pushed the fix/macos-device-io-resume branch from 0becae2 to 81c9b66 Compare September 17, 2026 11:09
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.

1 participant