Skip to content

fix(agent): re-arm a crash respawn instead of going dormant - #1619

Merged
AprilNEA merged 2 commits into
masterfrom
fix/agent-rearm-after-crash
Sep 29, 2026
Merged

AprilNEA merged 2 commits into
masterfrom
fix/agent-rearm-after-crash

Conversation

@AprilNEA

Copy link
Copy Markdown
Owner

Summary

With launch_at_login off, the macOS dormancy gate treats every launchd start as a login the user opted out of, and leaves 60 s later with the exit(0) launchd never respawns. A crash respawn takes the same path, because the service plist gives login and crash one trigger (SuccessfulExit implies RunAtLoad). So one non-zero exit — the hook watchdog's exit(78) on a sleep transition (#952), a panic — silently ended remapping until the GUI was opened by hand. On the reporting host that was 3.5 h on 2026-09-28 and the rest of the night on 2026-09-26; the agent log shows the sequence HID CGEventTap lifecycle did not make progress → launch_at_login is off — dormant until a client demands arming → no arming demand — exiting until wanted.

The gate now asks the login session instead of guessing at launchd's reason. Arming records the session; a start that finds its own session recorded follows an unclean exit or a handover and re-arms at once; a login finds nothing or a stale record and stays dormant. With #1282 (the watchdog half) this is the second half of #952.

Changes

  • openlogi-agent: new lifecycle/armed_session.rs. Arming records the login session — kernel boot session UUID (kern.bootsessionuuid) plus audit session id (SessionGetInfo) — in the runtime dir; the boot half is needed because audit ids repeat across boots. Running::shut_down (tray Quit, uninstall, SIGTERM/SIGINT) clears the record before leaving; Running::hand_over (Input Monitoring relaunch) and exit_after_replacement_teardown (binary update) leave it for the successor. Booted::gate re-arms when armed_session::rearm() is true, before the dormant wait. New macOS-only dependencies objc2-security (workspace table, AuthSession feature) and libc; Cargo.lock gains only those two edges, no version moves.
  • docs/DECISIONS.md: why the gate asks the session rather than launchd, why a second crash-only plist (KeepAlive = {Crashed: true}) was rejected (it does not respawn exit(78) or a panic's exit(101), and it reintroduces the two-label registration the 2026-08 lifecycle work removed), and why SIGTERM is final.
  • .agents/rules/objc-ffi.md: the new unsafe site and the new framework crate in the inventory. The xtask plist doc comment points at the decision.

Behaviour changes, all with launch_at_login off: a launchd respawn after a crash re-arms; the successor after a Homebrew or in-app update re-arms even with no GUI running (previously it went dormant and exited after 60 s); the relaunch after an Input Monitoring grant re-arms likewise. Login starts are unchanged: a new login has a new audit session id.

Testing

Run on macOS 26 (arm64) with RUSTFLAGS="-D warnings", on the tree rebased onto master 5cca3f5:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items --exclude openlogi-ui --exclude openlogi-desktop --exclude openlogi-overlay --exclude openlogi-agent
  • cargo xtask ci clippy-windows (the hand_over/leave split is what the Windows lane checks; an earlier shape failed there with a dead-code variant)

Not run: tests (linux), msrv, cargo-deny (CI).

Runtime, a debug agent in an isolated XDG sandbox (dev profile, launch_at_login = false, capture_mouse_events = false, socket path kept under the 104-byte sun_path limit): fresh start goes dormant and writes no record; a ClientKind::Gui declaration arms it and writes the record; SIGKILL (stand-in for exit(78), no lifecycle exit runs) leaves the record and the next start logs re-arming; SIGTERM clears the record and the next start goes dormant; a planted record from another audit session id or another boot UUID leaves the start dormant.

Not runtime-tested on the packaged, launchd-supervised build, and no real lid-close cycle. To test on hardware: with launch_at_login off and the agent armed, kill -KILL $(pgrep -x openlogi-agent); launchd respawns it and the agent log should read launch_at_login is off, but this login armed an agent that did not quit — re-arming instead of the dormant line. Then Quit from the tray and launchctl kickstart gui/$(id -u)/org.openlogi.agent.service: the log should read dormant until a client demands arming and the agent should leave after 60 s.

One test, the_current_session_is_readable_and_round_trips, asserts that the test process has an audit session and that kern.bootsessionuuid reads back; it passes on a logged-in Mac and is expected to on the macOS runner.

Refs #952

With launch_at_login off, the macOS dormancy gate read every launchd start as a login the user opted out of and left 60 s later with the exit(0) launchd never respawns. A crash respawn takes the same path, since the service plist gives login and crash one trigger, so a single hook-watchdog exit(78) on a lid close ended remapping until the GUI was opened by hand.

Arming now records the login session (kernel boot session UUID plus audit session id) in the runtime dir. Every final exit - tray Quit, uninstall, SIGTERM - erases it; a handover to a scheduled successor keeps it. A start that finds its own session recorded re-arms at once; a login finds a stale record and stays dormant.

Refs #952
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Agent crash recovery now checks login session state.

The PR appears safe to merge; no new actionable issue remains.

Summary

The PR records the macOS login session when the agent arms, allowing a crash respawn or scheduled successor to re-arm while keeping a new login dormant. The latest changes stage record writes before renaming and clear the record on the tray-Quit fallback.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Agent starts with launch_at_login off] --> B{Recorded session matches?}
  B -->|Yes| C[Re-arm]
  B -->|No| D[Wait dormant for GUI demand]
  D -->|GUI declares| E[Arm and record session]
  C --> E
  E --> F{How does agent leave?}
  F -->|Final exit or tray Quit fallback| G[Clear record]
  F -->|Crash or scheduled handover| H[Keep record for successor]
Loading

Reviews (2) · Last reviewed commit: "fix(agent): keep the armed-session recor..."

Comment thread crates/openlogi-agent/src/lifecycle/armed_session.rs Outdated
Comment thread crates/openlogi-agent/src/lifecycle.rs
…uit fallback

Two review findings on #1619. The record was written in place, so a kill mid-write could leave a truncated file the next start reads as no record; it is now staged beside the record and renamed into place. And the tray-Quit fallback in shutdown.rs, taken when the lifecycle that would clear the record has already ended, exited without clearing it; it now does, so a Quit whose core had died is still followed by a dormant start rather than a re-arm.
@AprilNEA
AprilNEA merged commit 858a483 into master Sep 29, 2026
26 checks passed
@AprilNEA
AprilNEA deleted the fix/agent-rearm-after-crash branch September 29, 2026 11:19
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