Unresponsive app recovery specification.
Purpose
Detect GUI processes that macOS marks as not responding. Recover a confirmed unresponsive process with the correct safety policy.
Scope
- In scope:
- User-visible macOS apps in the current login session.
- Explicitly allowlisted system GUI services.
- The same unresponsive state that macOS shows in Force Quit.
- Automatic restart with confirmation and cooldown limits.
- Automatic recovery for allowlisted system services.
- Status and event history.
- Out of scope:
- Background daemons and helper processes that are not allowlisted.
- Apple system apps and services that are not allowlisted.
- Diagnosis of the cause of an app hang.
- Recovery of unsaved app data.
Persistence
Entities
UnresponsiveAppMonitorobservations: A map keyed by app identity.restarts: The last automatic restart time for each app identity.blocked: App identities that already caused a user notification.
AppIdentity- Use the bundle identifier when it is available.
- Otherwise, use the absolute app bundle path.
Event- Use the existing DETS event store.
- Store the app identity, display name, PID, and bundle path in event details.
Lifecycle
- The first unresponsive observation creates a pending observation.
- A responsive or stopped app clears its pending observation.
- Two consecutive unresponsive observations confirm a hang.
- A confirmed hang starts an automatic restart when the cooldown permits it.
- A confirmed allowlisted service restarts without prompting or notifying the user.
- An app that stays unresponsive during cooldown becomes blocked.
- A responsive or stopped app clears its blocked state.
Constraints and Indexes
- An app needs two consecutive observations before a restart.
- An app can restart at most once per hour.
- Restart timestamps older than 24 hours can be removed.
- The monitor must not create atoms from app names or bundle identifiers.
Retention and Privacy
- Data stays in the existing local canaryd DETS files.
- The monitor does not store document content or command-line arguments.
- Existing event retention behavior applies.
Relationships
Canaryd.Checkerruns the monitor during every check round.- Idle-user checks still scan the macOS responsiveness state.
- The CleanClip functional probe keeps its existing behavior.
Behavior
- Query the WindowServer responsiveness state for running GUI apps.
- Clear pending observations and stop without restart actions when the macOS interface is unavailable.
- Keep third-party apps with a regular activation policy.
- Exclude Apple bundle identifiers and app bundles under system paths by default.
- Include
com.apple.TextInputUI.xpc.CursorUIViewServiceas an explicit automatic service only when its activation policy and system XPC path also match. - Confirm an unresponsive process in two consecutive check rounds.
- Restart a confirmed third-party app or allowlisted service automatically.
- Do not notify the user after a successful automatic restart.
- Send
SIGTERMto stop the currentCursorUIViewServiceinstance. - Send
SIGKILLif the service does not stop within the grace period. - Ask launchd to start the exact service job in the current user domain after the old PID stops.
- Wait up to five seconds for the replacement service PID.
- Log a
CursorUIViewServicerestart failure without notifying the user. - Send
SIGTERMto a confirmed third-party app. - Send
SIGKILLonly when the same app process does not stop within the grace period. - Wait for the old app PID to stop.
- Open the same third-party app bundle in the background.
- Notify once when a third-party app is confirmed unresponsive again during restart cooldown.
- Log detection, restart, restart failure, and blocked events.
BDD Scenarios
BDD-01 Restart CursorUIViewService without prompting
Given:
CursorUIViewServicematches the allowlisted identity and system XPC path.- It is unresponsive in two consecutive check rounds.
When:
- canaryd evaluates the second observation.
Then:
- canaryd automatically stops the old PID.
- canaryd requests the exact launchd job in the current user domain.
- canaryd waits for a replacement PID.
- service recovery never sends a notification, including when restart confirmation fails.
Test Plan:
- Lowest useful level: unit tests for the service command sequence, allowlisting, monitor actions, and notification policy.
- First failing test: service restart requests
launchctl kickstartfor the current user after the old PID stops. - Follow-up tests: the replacement PID is accepted and service recovery is marked silent.
Acceptance Evidence:
Canaryd.Apps.UnresponsiveTestlaunchd restart, allowlist, recovery-mode, and notification-policy tests.Canaryd.UnresponsiveMonitorTestautomatic service restart test.- Local production-path restart that replaces the real service PID without a notification.
Automatic termination can discard unsaved data. Exact service identity checks, confirmation, and cooldown limits reduce this risk.
Search
- Not applicable.
AI
- Not applicable.
Cross-Spec Links
Open Questions
- None.
Acceptance Record
| Scenario | Status | Evidence | Notes |
|---|---|---|---|
| BDD-01 | passed | Canaryd.Apps.UnresponsiveTest, Canaryd.UnresponsiveMonitorTest, 105-test full suite, local production-path restart, installed-binary checksum and launchd checks | The real service changed from PID 7103 to PID 8007 and returned :ok; the exact service identity is covered by the silent recovery policy. |