Skip to content

perf: move meeting-prompt EKEventStore queries off the main actor#1070

Merged
r3dbars merged 1 commit into
mainfrom
claude/amazing-northcutt-4529c5
Jun 12, 2026
Merged

perf: move meeting-prompt EKEventStore queries off the main actor#1070
r3dbars merged 1 commit into
mainfrom
claude/amazing-northcutt-4529c5

Conversation

@r3dbars

@r3dbars r3dbars commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Why

MeetingPromptDetector ran synchronous EKEventStore.events(matching:) queries on the main actor: every 20s poll, every supported-app activation, and again on each runtime-prompt dismiss via nextRuntimePromptResumeDate. On large calendars each query blocks the main thread, causing recurring jank in a menubar app that should be invisible.

Product Impact

  • Affects: meetings
  • Lane: meeting reliability
  • Why this matters: prompt detection should never make the whole app stutter every 20 seconds for users with busy calendars.

What changed

  • New MeetingPromptCalendarReader owns the EKEventStore and runs queries on a background utility queue, bridged with a checked continuation. The store is never queried on the main thread.
  • Each poll cycle does one broad fetch (now-5min .. now+12h, covering both the prompt window and the 12h dismiss-resume lookahead) instead of a narrow query per poll plus ad-hoc 12h queries on dismiss/accept.
  • Events are converted to plain-value MeetingPromptCalendarEventSnapshots on the reader's queue (EKEvent objects must not cross threads); only Sendable values hop back to the main actor. This also moves the NSDataDetector meeting-URL scan off main.
  • All paths that must stay synchronous — dismiss/remindSoon/markAccepted backoff decisions and currentSuggestedTranscriptTitle() (consumed by MeetingSessionController's sync title closure) — read the main-actor snapshot cache instead of querying live.

Semantics are preserved: the old narrow query window was strictly looser than the shouldOfferCalendarPrompt scoring filter, so cache-derived candidates are identical, and prompt scoring, snooze/pending bookkeeping, and MeetingPromptHeuristics are untouched. Dismiss resume dates read a cache at most one 20s poll stale — a prompt can only be dismissed after the evaluate cycle that refreshed the cache.

How I checked it

  • scripts/dev/agent-preflight.sh
  • Selected checks from .agents/test-matrix.yml for the files changed (Sources/Meeting/** rule: bash build-deps.sh --force + build + fast tests + integration smoke, all run bare)
  • bash build.sh --no-open
  • bash run-tests.sh — 4655/4655 passed, warning-free compile; MeetingPromptDetectorTests and MeetingPromptHeuristicsTests green and unmodified
  • Performance budget passed (bash build.sh --no-open bundle gate)
  • bash run-integration-smoke.sh if I touched Sources/Meeting/ or Sources/TranscriptedCore/
  • swift test — not required (no Package.swift / Sources/TranscriptedCore/ changes)
  • Manual check: none (no UI change)

Risk Review

  • Privacy / local-first behavior reviewed — no off-device payload changes; calendar data stays in-process
  • Storage path or migration impact reviewed — none
  • Public-facing copy stays concrete and matches current product scope — no copy changes
  • Release/update impact reviewed — none
  • Agent PRs link the issue/workpad and stay draft until human review
  • UI changes include sanitized .agent-review/visuals/ evidence — n/a
  • No private transcripts, audio, tokens, personal paths, or customer data are included

Notes

One behavior nuance: currentSuggestedTranscriptTitle() now reads the latest polled snapshot rather than doing a fresh blocking query, so a recording started within the first second after launch (before the first poll lands) could miss the title hint. In exchange, recording start no longer pays a synchronous calendar query.

Agent handoff

COORD_DONE: GREEN | PR URL below | moved MeetingPromptDetector EKEventStore queries to a background reader with a main-actor snapshot cache | none | none | build-deps --force, build.sh --no-open, run-tests.sh (4655 pass), run-integration-smoke.sh | human review + merge

🤖 Generated with Claude Code

MeetingPromptDetector ran synchronous EKEventStore.events(matching:)
queries on the main actor: every 20s poll, every workspace-app
activation, and again on each runtime-prompt dismiss via
nextRuntimePromptResumeDate. Large calendars caused recurring
main-thread jank in the menubar app.

Each poll cycle now does a single broad fetch (now-5min .. now+12h) on
a background utility queue via MeetingPromptCalendarReader, converting
events to Sendable snapshots on that queue (EKEvent objects must not
cross threads), then hops back to the main actor with plain values.
The synchronous paths — dismiss/remindSoon/markAccepted backoff
decisions and the recording-start title hint — read the cached
snapshots instead of querying live, so overlay callbacks and the
MeetingSessionController title closure keep their sync signatures.

Prompt scoring, snooze/pending bookkeeping, and heuristics are
unchanged: the old narrow query window was strictly looser than the
shouldOfferCalendarPrompt filter, so cache-derived candidates are
identical, and dismiss resume dates use a cache at most one 20s poll
stale (a prompt can only be dismissed after the evaluate cycle that
refreshed the cache).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@r3dbars r3dbars marked this pull request as ready for review June 12, 2026 17:00
@r3dbars r3dbars merged commit f50a96b into main Jun 12, 2026
1 check 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.

1 participant