Skip to content

fix(calendar): singleton EKEventStore + macOS 26 stale status fix#3948

Merged
louis030195 merged 3 commits into
screenpipe:mainfrom
divanshu-go:fix/calendar-singleton-plus-macos26-stale-status
Jun 10, 2026
Merged

fix(calendar): singleton EKEventStore + macOS 26 stale status fix#3948
louis030195 merged 3 commits into
screenpipe:mainfrom
divanshu-go:fix/calendar-singleton-plus-macos26-stale-status

Conversation

@divanshu-go

@divanshu-go divanshu-go commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What

Two combined fixes for Apple Calendar permissions on macOS:

  1. Singleton EKEventStore — eliminates 30+ redundant EKEventStore instances
  2. macOS 26 stale authorization status — events list immediately after grant without requiring relaunch

Root Cause

Issue 1: Multiple EKEventStore instances

Every ScreenpipeCalendar::new() created two EKEventStore instances (one inside EventsManager, one explicit). With 15+ calls across the app, that's 30+ instances — triggering Apple's EventKit anti-spam protection, causing multiple permission prompts and eventually blocking the app entirely.

Issue 2: macOS 26 stale authorization status (supersedes #3936)

authorizationStatusForEntityType: keeps returning a non-FullAccess value for minutes after an in-process grant. request_access() returned granted = true, but every read rebuilt a fresh EKEventStore, re-checked the stale static status, and hard-failed with AuthorizationDenied. Result: GET /connections/calendar/events returned HTTP 500 in a loop until the OS cache refreshed or the user relaunched.

Fix

Singleton

  • OnceLock<SingletonManager> ensures exactly ONE EventsManager/EKEventStore is ever created
  • All 15+ call-sites share the same instance via get_singleton()
  • Thread-safe: Send + Sync traits on the wrapper

Trust in-process grants

  • ACCESS_GRANTED_THIS_SESSION flag remembered after a successful request_access()
  • Read gate: FullAccess → read; Denied/Restricted → block; anything else with a session grant → call reset() and read anyway
  • Logging bumped from debug! to info!/warn! so the next occurrence is visible in logs

Impact

Metric Before After
EKEventStore instances 30+ 1
Permission prompts Multiple Single
EventKit blocking Yes No
Events after grant Delayed / empty Immediate

References

…ion prompts

- Replace multiple EKEventStore instances with singleton pattern
- Each ScreenpipeCalendar::new() created 2 EKEventStore instances (30+ total)
- This caused multiple permission prompts and EventKit blocking
- Now all calls share ONE EventsManager/EKEventStore via OnceLock
- Thread-safe with Send + Sync traits
- Reduces EKEventStore instances from 30+ to 1
- Prevents EventKit blocking and permission spam
- All 15+ calls across the app now reuse the same instance
macOS 26's static authorizationStatusForEntityType: can keep returning a
non-FullAccess value for minutes after an in-process grant. request_access()
returned granted, but every subsequent read rebuilt a fresh EKEventStore,
re-checked the stale static status, and hard-failed with AuthorizationDenied.
A freshly connected calendar therefore returned HTTP 500 on
/connections/calendar/events and listed nothing, so users re-clicked Connect
in a loop until the OS cache eventually refreshed (or they relaunched).

- Remember a successful grant in-process (ACCESS_GRANTED_THIS_SESSION).
- Read gate: FullAccess reads; Denied/Restricted block; anything else with a
  grant seen this session calls reset() and reads anyway instead of failing.
- Bump macOS calendar read logging debug -> info/warn and log the live auth
  status on failure (Windows already did this) so the next case is visible.
- Combined with singleton pattern: ONE EKEventStore instance + trust in-process
  grants = reliable calendar permissions on macOS 26+.
@divanshu-go

Copy link
Copy Markdown
Contributor Author

@louis030195

Screen.Recording.2026-06-10.at.4.10.14.AM.mov

@divanshu-go

Copy link
Copy Markdown
Contributor Author

@louis030195 This PR supersedes #3936

@louis030195 louis030195 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice. @divanshu-go two small things:

generated by the screenpipe pr-review pipe (https://screenpi.pe), not written by a human — reply and tag @louis030195 if it got something wrong.

let status = ScreenpipeCalendar::authorization_status();
if format!("{}", status) != "Full Access" {
if format!("{}", status) != "Full Access"
&& !ScreenpipeCalendar::access_granted_this_session()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why format status to string? can we use an is_authorized() helper?

})
.0
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is Sync safe without a mutex? ekeventstore shouldn't be used concurrently

@louis030195 louis030195 merged commit 133738d into screenpipe:main Jun 10, 2026
11 checks passed
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.

2 participants