Skip to content

modern concurrency#93

Open
KishanBagaria wants to merge 69 commits into
mainfrom
kb/modern-concurrency
Open

modern concurrency#93
KishanBagaria wants to merge 69 commits into
mainfrom
kb/modern-concurrency

Conversation

@KishanBagaria

@KishanBagaria KishanBagaria commented May 31, 2026

Copy link
Copy Markdown
Member

What

Moves the entire Messages.app automation path off the legacy DispatchQueue + UnfairLock + semaphore/Thread.sleep model and onto Swift structured concurrency (actors + async/await), with real cancellation, bounded retries, and unit-testable seams. Net: +1,786 / −1,201 across 34 files.

The old model serialized automation by hopping onto a PassivelyAwareDispatchQueue and blocking; sleeps and waits pinned threads, and there was no way to cancel an in-flight operation. The new model funnels every operation through one serial async lane, propagates cancellation cooperatively, and bounds the retry/invalidation loops so a stuck controller can't spin forever.

Key changes

MessagesControllerAutomationLane (new actor) replaces PassivelyAwareDispatchQueue. A single serial async lane that all MessagesController automation funnels through (PlatformAPI.runOnMessagesControllerLane), so only one operation touches Messages.app at a time. It handles:

  • Ordering — each action queues behind the tail of previously-submitted work.
  • Cancellation — caller cancellation propagates to the enqueued task.
  • Passive idle callbacks — epoch-guarded, fire after idleDelay and keep firing while the lane stays quiet; new active work cancels a stale idle cycle.
  • Re-entrancy safety — a @TaskLocal flag turns re-entry into a loud precondition crash rather than a silent deadlock. (Documented why an "inline fallback" is unsafe: the task-local leaks into spawned Task {} children.)

PlatformAPI / MessagesControllerCoordinator — controller actions are now async throws; onMessagesControllerQueuerunOnMessagesControllerLane. The coordinator's invalidation loop is now bounded (max 30) with Task.checkCancellation() checks, and disposal runs inside a cancellation-immune unstructured Task so cancelling a caller mid-construction can no longer leak a half-launched Messages.app.

MessagesController — nearly all automation methods converted from sync throws to async throws (send, react, edit, undo-send, mute, delete, typing, mark-read, deep-link open, etc.). prepareForAutomation/finishedAutomation replaced by a scoped withAutomation { … } / automationDidComplete. Dead paths removed (markAsReadWithMenu, revealReplyTranscriptViaMenu).

Window coordinatorsmakeAutomatable/reset are now async throws, with explicit @MainActor annotations and MainActor.run hops where AppKit work happens. The coordinator protocol is intentionally not @MainActor (implementations do blocking Accessibility I/O that would freeze the UI on the main actor; they hop to @MainActor only for the AppKit bits) — documented inline.

New async primitives:

  • NSWorkspace+AsyncOpen — bridges NSWorkspace.open(…completionHandler:) to async with a single-completion-point continuation, timeout, and cancellation handling. Used for deep-link opens.
  • retry(withTimeout:) — cancellation-aware async retry (re-throws CancellationError, swallows/logs onError failures).
  • Result(catching:) async initializer.

KeyPresser — refactored for testability with injectable key-post and keymap closures (defaults preserve real behavior). Key presses now default to onMainThread: false since the lane is no longer the main thread.

Extensions / IMessageHostwaitForLaunch and related waits converted from Thread.sleep to Task.sleep.

Tests

  • New: MessagesControllerAutomationLaneTests (232 lines — ordering, idle callbacks, re-entrancy, cancellation), NSWorkspaceAsyncOpenTests, RetryTests, KeyPresserTests, and an Eventually async test helper.
  • Removed: PassivelyAwareDispatchQueueTests (along with its subject).

Follow-ups (tracked in todos.md)

  • Adopt -strict-concurrency=targeted and clear the ~26 concurrency warnings with real Sendable/isolation annotations (not blanket @unchecked). Surfaced by plan-eng-review; deferred so it can land as a focused pass with its own QA.
  • Add a factory seam to MessagesControllerCoordinator so the dedup/invalidate/dispose logic is unit-testable without Accessibility + Messages.app.
  • Make withAutomation cleanup fire when makeAutomatable partially succeeds (pre-existing edge: a throw after unhide/resize can leave Messages.app visible with stale windowFramePreEclipse).

@indent-zero

indent-zero Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Migrates MessagesController from PassivelyAwareDispatchQueue + UnfairLock to modern Swift Concurrency (actor-based automation lane, @MainActor, async/await), and replaces hand-rolled continuation bridging with reusable async primitives.

  • Introduced MessagesControllerAutomationLane actor with @TaskLocal isExecutingOnLane re-entrancy guard and a MessagesControllerCoordinator actor capping invalidations.
  • Split lifecycle/dispose handling into dedicated files; eliminated duplicate AX fetches; consolidated partIndex/axOffset after merging main.
  • Migrated PromptAutomation and MessagesAppElements' deep-link path to async/await; added lastOpenedThreadID tracking via a new MessagesDeepLink.targetThreadID.
  • Refactored onThreadSelected and added RCS thread support; reworked SelectedThreadActivityState as a closure-driven state machine.
  • Added withTimeout primitive (Task+Timeout.swift) and KVO+AsyncStream extension (asyncValues(for:), waitForValue(...), AsyncSequence.first(until:where:)).
  • Rewrote NSRunningApplication.waitForLaunch on top of withTimeout + withThrowingTaskGroup with two KVO waiters (finished-launching success, terminated → throw), restoring termination detection and prompt cancellation.
  • Removed ~88 lines of custom NSWorkspace+AsyncOpen.swift bridging in favor of native NSWorkspace.shared.open(_:configuration:)/openApplication(at:configuration:) wrapped in withTimeout; replaced NSWorkspaceAsyncOpenTests with broader AsyncUtilityTests covering timeout, cancellation, KVO wait, and a zero-timeout integration test.

Issues

All clear! No issues remaining. 🎉

12 issues already resolved
  • SystemSettingsOnboarding.start()/stop() are now @MainActor, but call sites in IMessageCLI.swift:121-122 (non-isolated async request(), including a defer { SystemSettingsOnboarding.stop() } that cannot await) and IMessageNodeExports.swift:60, 63 (NodeFunction closures) were not updated to await/hop isolation, which will at minimum produce concurrency warnings and become errors under stricter checking. (fixed by commit fb8607d)
  • Lifecycle activate/deactivate now serialized through the automation lane: user-initiated focus/blur of Messages.app blocks behind any in-flight send/edit/react (these can take several seconds, e.g. editMessage's 6s retry), so the eclipse will not unhide responsively and lifecycle AX events stack up in the for await loop until the lane drains. (fixed by commit 39d5074)
  • [Still open — suggestion in 3b452a2 was misapplied; see critical f56bb2ae] PromptAutomation.disableNotificationsForApp retains the // hides shows a gray background and doesn't render the UI warning text but no longer sets configuration.hides = true (it now sets configuration.activates = false twice instead, alongside duplicated let app = ... lines that break compilation). Resolving f56bb2ae per the corrected snippet there also resolves this one. (fixed by commit d8572c1)
  • In the new toggleThreadRead, if the first try await triggerThreadCellAction(threadID:, action: .pin) throws the function returns without calling restorePinsIfNecessary(). The old code's outer defer ran the restore in this case too; this is a small regression for the edge case where the pin action throws mid-way but the pin count nevertheless changed. (fixed by commit 339e1c5)
  • OnboardingManager.createWindow() no longer initializes the window synchronously: the timer body now dispatches a Task { @MainActor … }, so the immediate pollingTimer?.fire() only schedules work asynchronously and onboardingWindow is nil for one runloop turn after the call returns. (fixed by commit a8f6134)
  • The rewritten NSRunningApplication.waitForLaunch no longer detects mid-launch termination: the old polling loop checked isTerminated every interval and threw "… terminated", but the new Combine pipeline only listens to isFinishedLaunching, so a Messages.app that crashes between launchApplication and isFinishedLaunching == true will sit silently until the 5s/30s timeout then return success — callers will treat a dead PID as launched. (fixed by commit 74e97c5)
  • Latent deadlock: any caller already running on the automation lane that re-enters runOnMessagesControllerLane will hang permanently (the tail-chained enqueue makes the new task await the still-running ancestor task). Safe today because withAutomation and the idle observer never re-enter, but nothing in code or comments warns against this, so a future contributor adding a withMessagesController call inside an automation body or idle callback will silently freeze the lane. (fixed by commit ea4da68)
  • MessagesController.openDeepLink now imposes a 5s default timeout on the LaunchServices wait (previously the DispatchSemaphore waited indefinitely), so a cold Messages.app launch from MessagesController.init that takes longer than 5s — first run after reboot, slow disks, system under load — will fail init with "Timed out opening URL via LaunchServices after 5.0s" where it would have succeeded before. (fixed by commit 39d1e43)
  • NSRunningApplication.waitForLaunch no longer responds to task cancellation: the polling loop's try await Task.sleep(...) threw CancellationError within ≤50ms of cancel, but the new withCheckedThrowingContinuation has no withTaskCancellationHandler, so a cancelled caller stays parked in the continuation until the Combine .timeout (up to 30s on the cold-launch path) fires — regressing the lane's cooperative cancellation latency the rest of the PR worked to guarantee. (fixed by commit 74e97c5)
  • HideDebouncer sink now schedules the hide via Task { @MainActor } instead of calling app.hide() synchronously, so a subsequent immediatelyUnhide() (e.g. the next automation cycle's makeAutomatable) that runs on main after the sink fires but before the Task runs will unhide the window and then be immediately overwritten by the pending app.hide() — reintroducing the exact hide/unhide flicker the debouncer was built to prevent. Trigger: immediatelyUnhide called within the sub-millisecond gap between the debounce firing and the scheduled main-actor Task running. Replace the Task { @MainActor [weak self] in … } with MainActor.assumeIsolated { … } (the debounce is already delivered on RunLoop.main) so the hide runs synchronously in the sink like it did previously. (fixed by commit 16bb5d6)
  • MessagesAppElements's default openDeepLink closure now calls fire-and-forget MessagesController.requestDeepLinkOpen($0) instead of the previous semaphore-blocked open. Inside _mainWindowReally's onError recovery, attempt 0 issues the compose deep link and immediately returns, so the retry loop's 0.2s polling can iterate (and even exit) before LaunchServices has actually processed the open — losing the implicit "deep link has been requested and processed" synchronization the old code provided. (fixed by commit 7d149e8)
  • PromptAutomation.swift no longer compiles: applying the suggestion duplicated configuration.activates = false and the let app = try await NSWorkspace.shared.open( opener while leaving the original lines in place — there are now two let app declarations and an extra ( that is never closed (lines 70–80). Build is broken and the hides = true regression that prompted the suggestion is still NOT fixed (the misapplied suggestion only added the no-op duplicate; the actual configuration.hides = true warning text is gone but the bug structure was replaced by malformed code). (fixed by commit d8572c1)

CI Checks

Both Buildkite jobs (Test & Build and Build, Sign & Notarize CLI) are failing, but the logs aren't publicly accessible so I can't pin the exact failure. Worth noting the same two checks have been red on every tested commit on this branch going back at least to aef2925 (LaunchServices timeout wrap) and through b31da6c (withTimeout rewrite), d4d991f (fable i1), the 16bb5d6 revert, and now 672cc08 — so these look preexisting rather than caused by the main-merge, and neither failure has been triggered by anything the merge itself touched (the merge only conflicted in PlatformAPI.dispose, and I verified the resolution is consistent with EventWatcherLifecycle.cancelWatchingIfNecessary now being sync per PR #101). Grabbing the compiler output from Buildkite is the next step to know which target/file is red.

Failing Test & Build
  • yarn test:swift (swift test) failed in ~2m34s on the Mac agent (Xcode 26.3). Buildkite requires auth so the log isn't retrievable from here — the same job has been failing continuously since roughly aef2925/b31da6c, so the root cause almost certainly predates the merge (my best guess is a swift test-only failure in Task+Timeout/waitForLaunch-adjacent tests, or a KeyPresser/RetryTests target that only surfaces under swift test). Please open the Buildkite job and share the failing target/file so I can propose a fix.
Failing Build, Sign & Notarize CLI
  • swift build -c release --product imessage-cli (via .buildkite/commands/release-cli.sh) failed in ~4m11s. Same pattern as Test & Build — red on every tested commit since aef2925, so it looks preexisting rather than merge-caused. Since it also fails release-mode compilation (not just tests), the culprit is likely a Swift 5.9-mode compile error in a non-test target that Xcode 26.3 rejects. Buildkite log isn't publicly accessible; sharing the failing file/error would let me pinpoint the fix.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates automation from a DispatchQueue-based idle queue to async/await: it adds a MessagesControllerAutomationLane actor, converts MessagesController and PlatformAPI to async/await with new automation boundaries, applies @MainActor to UI/window components, removes PassivelyAwareDispatchQueue and its tests, and adds supporting async utilities and tests.

Changes

Concurrency Infrastructure and MessagesController Async Migration

Layer / File(s) Summary
Automation lane and PlatformAPI wiring
src/IMessage/Sources/IMessage/PlatformAPI+MessagesController.swift, src/IMessage/Sources/IMessage/PlatformAPI.swift
Add MessagesControllerAutomationLane actor; expose PlatformAPI.runOnMessagesControllerLane and setMessagesControllerIdleCallback; move controller creation/disposal and PlatformAPI callsites to use async lane-run closures and register idle callbacks via the lane.
MessagesController async/await conversion and withAutomation
src/IMessage/Sources/IMessage/Messages/MessagesController.swift, src/IMessage/Sources/IMessage/Messages/MessagesController+PlatformOperations.swift
Convert initializer and many user-interaction methods to async/throws; introduce withAutomation/withActivation helpers; convert deep-link open and thread-opening flows to async; update platform-operation wrappers to async/throws.
Remove old automation infra, tests, and TestBench hooks
src/IMessage/Sources/IMDatabaseTestBench/TestBench.swift, todos.md, src/IMessage/Sources/IMessageCore/PassivelyAwareDispatchQueue.swift, src/IMessage/Sources/IMessageTests/PassivelyAwareDispatchQueueTests.swift
Remove TestIdleAware CLI command, delete PassivelyAwareDispatchQueue implementation and its tests, and remove the related TODO entry.
@MainActor isolation: window coordination and UI managers
src/IMessage/Sources/IMessage/WindowCoordination/..., src/IMessage/Sources/IMessage/Extensions.swift, src/IMessage/Sources/IMessage/OnboardingManager.swift, src/IMessage/Sources/IMessage/SystemSettingsOnboarding.swift, src/IMessage/Sources/IMessageCLI/IMessageCLI.swift
Apply @MainActor to WindowCoordinator protocol and implementations (Eclipsing, Spaces), annotate UI helpers and managers (NSRect extension, OnboardingManager, SystemSettingsOnboarding, AuthorizationRequirement.request) and remove DispatchQueue.main.sync/async bridges.
MessagesControllerAutomationLane tests
src/IMessage/Sources/IMessageTests/MessagesControllerAutomationLaneTests.swift
Replace prior idle-queue tests with async tests for the new lane: serialization, ordering, idle callback semantics, cancellation, error isolation, and return-value behavior.
Async helpers, Node exports, and CLI
src/IMessage/Sources/IMessage/Utilities/NSWorkspace+AsyncOpen.swift, src/IMessage/Sources/IMessageCore/Result+Async.swift, src/IMessage/Sources/IMessageNode/IMessageNodeExports.swift, src/IMessage/Sources/IMessage/Messages/MessagesAppElements.swift
Add NSWorkspace.open async wrapper with timeout, add Result.init(catching:) async initializer, export SystemSettingsOnboarding start/stop as async NodeFunctions dispatching to MainActor.run, and change MessagesAppElements default deep-link opener to requestDeepLinkOpen.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • beeper/platform-imessage#92: Overlaps on coordinate-space handling and window coordination changes (NSRect flipping and EclipsingWindowCoordinator adjustments).
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the concurrency modernization effort, including key changes like PassivelyAwareDispatchQueue removal, async/await conversion, and @MainActor annotations.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'modern concurrency' accurately describes the main objective of the changeset, which involves converting synchronous APIs to async/await and applying @MainActor annotations throughout the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kb/modern-concurrency

Comment @coderabbitai help to get the list of available commands.

Comment thread src/IMessage/Sources/IMessage/Messages/MessagesController.swift
Comment thread src/IMessage/Sources/IMessage/PlatformAPI+MessagesController.swift Outdated
Comment thread src/IMessage/Sources/IMessage/OnboardingManager.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/IMessage/Sources/IMessage/Messages/MessagesController.swift (1)

1317-1349: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Manual activation/deactivation now wait behind unrelated automation.

These handlers are awaited from the AX event loop, but both now queue behind the same serial lane as long-running sends/edits/reactions. A busy automation run can therefore keep Messages hidden after the user manually activates it, then replay stale activate/deactivate transitions once the lane drains. The user-facing hide/unhide path needs to stay responsive; either keep the coordinator-facing part off-lane or coalesce pending lifecycle state before enqueueing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/IMessage/Sources/IMessage/Messages/MessagesController.swift` around lines
1317 - 1349, The activateMessages() and deactivateMessages() handlers are
getting queued behind long-running work because they call windowCoordinator and
window operations inside PlatformAPI.runOnMessagesControllerLane; fix by keeping
the coordinator-facing/window operations off that serial lane or by coalescing
state before enqueuing: move the user-facing state updates (lastActivate,
messagesIsManuallyActivated, log.debug) inside
PlatformAPI.runOnMessagesControllerLane but perform
windowCoordinator.userManuallyActivated(app), userManuallyDeactivated(app),
reset(window), closeAllNonMainWindows(), and resetWindow() outside the lane
(e.g., dispatch them asynchronously on a different queue or call a PlatformAPI
method that runs off the messages controller lane), or alternatively capture and
check the latest messagesIsManuallyActivated value inside the
runOnMessagesControllerLane closure before calling the coordinator to avoid
replaying stale transitions; update activateMessages() and deactivateMessages()
to use one of these approaches referencing activateMessages(),
deactivateMessages(), PlatformAPI.runOnMessagesControllerLane,
windowCoordinator.userManuallyActivated/userManuallyDeactivated, reset(window),
closeAllNonMainWindows(), and resetWindow().
src/IMessage/Sources/IMessage/PlatformAPI+MessagesController.swift (1)

18-21: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

@TaskLocal re-entry state leaks into spawned tasks.

TaskLocal values propagate into child Task {}s, so any delayed follow-up created inside a lane action inherits isExecutingOnLane = true and will hit this assertion even after the original lane work has finished. MessagesController.scheduleCancelReplyTranscriptView() now does exactly that, so normal automation will trap in debug; in release, true nested re-entry still hangs because assert disappears. This guard needs a non-inherited/token-based check, or a separate non-inheriting path for legitimate follow-up tasks.

Also applies to: 35-39, 63-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/IMessage/Sources/IMessage/PlatformAPI`+MessagesController.swift around
lines 18 - 21, The TaskLocal flag isExecutingOnLane wrongly propagates into
child Tasks and causes false positives in run and callers like
MessagesController.scheduleCancelReplyTranscriptView; fix by replacing the
propagation-prone guard with a non-inheriting mechanism or by ensuring follow-up
tasks don’t inherit TaskLocal state. Concretely: either (A) replace `@TaskLocal`
private static var isExecutingOnLane with a simple actor-based tracker/guard
(e.g., LaneExecutionTracker actor with enter()/exit()/isExecuting() used by run)
so spawned tasks don’t see a propagated boolean, or (B) keep the TaskLocal but
change callers that spawn delayed follow-ups (notably
MessagesController.scheduleCancelReplyTranscriptView) to create those follow-ups
with Task.detached { ... } (or otherwise clear the TaskLocal) so they won’t
inherit isExecutingOnLane; update the assertion/check in run to use the new
actor guard or the non-inheriting path accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/IMessage/Sources/IMessage/Messages/MessagesController.swift`:
- Around line 1317-1349: The activateMessages() and deactivateMessages()
handlers are getting queued behind long-running work because they call
windowCoordinator and window operations inside
PlatformAPI.runOnMessagesControllerLane; fix by keeping the
coordinator-facing/window operations off that serial lane or by coalescing state
before enqueuing: move the user-facing state updates (lastActivate,
messagesIsManuallyActivated, log.debug) inside
PlatformAPI.runOnMessagesControllerLane but perform
windowCoordinator.userManuallyActivated(app), userManuallyDeactivated(app),
reset(window), closeAllNonMainWindows(), and resetWindow() outside the lane
(e.g., dispatch them asynchronously on a different queue or call a PlatformAPI
method that runs off the messages controller lane), or alternatively capture and
check the latest messagesIsManuallyActivated value inside the
runOnMessagesControllerLane closure before calling the coordinator to avoid
replaying stale transitions; update activateMessages() and deactivateMessages()
to use one of these approaches referencing activateMessages(),
deactivateMessages(), PlatformAPI.runOnMessagesControllerLane,
windowCoordinator.userManuallyActivated/userManuallyDeactivated, reset(window),
closeAllNonMainWindows(), and resetWindow().

In `@src/IMessage/Sources/IMessage/PlatformAPI`+MessagesController.swift:
- Around line 18-21: The TaskLocal flag isExecutingOnLane wrongly propagates
into child Tasks and causes false positives in run and callers like
MessagesController.scheduleCancelReplyTranscriptView; fix by replacing the
propagation-prone guard with a non-inheriting mechanism or by ensuring follow-up
tasks don’t inherit TaskLocal state. Concretely: either (A) replace `@TaskLocal`
private static var isExecutingOnLane with a simple actor-based tracker/guard
(e.g., LaneExecutionTracker actor with enter()/exit()/isExecuting() used by run)
so spawned tasks don’t see a propagated boolean, or (B) keep the TaskLocal but
change callers that spawn delayed follow-ups (notably
MessagesController.scheduleCancelReplyTranscriptView) to create those follow-ups
with Task.detached { ... } (or otherwise clear the TaskLocal) so they won’t
inherit isExecutingOnLane; update the assertion/check in run to use the new
actor guard or the non-inheriting path accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89a3f762-fe9a-44ab-ad85-ee9bfc32d990

📥 Commits

Reviewing files that changed from the base of the PR and between 46e6f05 and ea4da68.

📒 Files selected for processing (6)
  • src/IMessage/Sources/IMessage/Messages/MessagesController.swift
  • src/IMessage/Sources/IMessage/PlatformAPI+MessagesController.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/SpacesWindowCoordinator.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/WindowCoordinator.swift
  • src/IMessage/Sources/IMessageTests/MessagesControllerAutomationLaneTests.swift
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-03T17:00:19.662Z
Learnt from: KishanBagaria
Repo: beeper/platform-imessage PR: 69
File: src/IMessage/Sources/IMessage/EventWatcher/EventWatcher+Updates.swift:89-93
Timestamp: 2026-05-03T17:00:19.662Z
Learning: In the beeper/platform-imessage Swift codebase, keep message IDs (`PlatformSDK.MessageID`) as raw GUIDs. When mapping from DB/event rows to `message.id`, set the ID directly from `msgRow.guid` (no GUID→public-ID hashing or transformation). For multi-part messages, append the part index as `_<part.index>` to the GUID-derived ID. During code review, if changes touch message ID creation/mapping, ensure this raw GUID + optional `_<part.index>` suffix behavior is preserved.

Applied to files:

  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/WindowCoordinator.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/SpacesWindowCoordinator.swift
  • src/IMessage/Sources/IMessageTests/MessagesControllerAutomationLaneTests.swift
  • src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift
  • src/IMessage/Sources/IMessage/PlatformAPI+MessagesController.swift
  • src/IMessage/Sources/IMessage/Messages/MessagesController.swift
🪛 SwiftLint (0.63.2)
src/IMessage/Sources/IMessageTests/MessagesControllerAutomationLaneTests.swift

[Warning] 25-25: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 51-51: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 62-62: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 72-72: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 82-82: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 100-100: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 116-116: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 142-142: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 154-154: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 20-20: Magic numbers should be replaced by named constants

(no_magic_numbers)

Comment thread src/IMessage/Sources/IMessage/Messages/MessagesController.swift Outdated
Comment thread src/IMessage/Sources/IMessage/Messages/MessagesAppElements.swift Outdated
Comment thread src/IMessage/Sources/IMessage/Messages/MessagesController.swift
indent-zero Bot and others added 8 commits May 31, 2026 11:22
…n throws too, so a partial pin state can still be cleaned up.

Generated with [Indent](https://indent.com)
Co-Authored-By: KishanBagaria <KishanBagaria@users.noreply.github.com>
…st so the _mainWindowReally retry path doesn't iterate before LaunchServices has processed the open.

Generated with [Indent](https://indent.com)
Co-Authored-By: KishanBagaria <KishanBagaria@users.noreply.github.com>
Comment thread src/IMessage/Sources/IMessage/Extensions.swift Outdated
Comment thread src/IMessage/Sources/IMessage/Extensions.swift Outdated
Comment thread src/IMessage/Sources/IMessage/PromptAutomation.swift
Comment thread src/IMessage/Sources/IMessage/WindowCoordination/HideDebouncer.swift Outdated
@pmanot pmanot marked this pull request as ready for review July 15, 2026 21:51
@pmanot pmanot self-assigned this Jul 15, 2026
Copilot AI review requested due to automatic review settings July 15, 2026 22:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR modernizes the Messages.app automation stack by replacing the legacy DispatchQueue/locks/semaphores/sleeps approach with Swift structured concurrency (actors + async/await), introducing a single serial “automation lane”, improved cancellation propagation, and bounded retry/timeout primitives to avoid runaway loops and improve testability.

Changes:

  • Introduces MessagesControllerAutomationLane (actor) and routes all MessagesController automation through it (PlatformAPI.runOnMessagesControllerLane), including idle-callback behavior and cancellation.
  • Converts major MessagesController/WindowCoordinator/PlatformAPI automation APIs from sync throws to async throws, adding targeted @MainActor hops for AppKit usage.
  • Adds new async utilities (Task.withTimeout, async retry(withTimeout:), async Result initializer, KVO async stream helpers) and corresponding unit tests; removes the old PassivelyAwareDispatchQueue and its tests.

Reviewed changes

Copilot reviewed 41 out of 41 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
todos.md Updates concurrency follow-ups (strict concurrency adoption, coordinator factory seam, withAutomation cleanup edge).
src/IMessage/Sources/IMessageTests/WaitForLaunchTests.swift Adds stress/behavior tests for async NSRunningApplication.waitForLaunch.
src/IMessage/Sources/IMessageTests/RetryTests.swift Adds coverage for new async retry overloads (timeout, cancellation, onError behavior).
src/IMessage/Sources/IMessageTests/PassivelyAwareDispatchQueueTests.swift Removes tests for deleted legacy idle-aware dispatch queue.
src/IMessage/Sources/IMessageTests/MessagesControllerAutomationLaneTests.swift Adds tests for the new serial automation lane actor (ordering, idle, cancellation, error isolation).
src/IMessage/Sources/IMessageTests/KeyPresserTests.swift Adds tests for KeyPresser’s new injection points and thread dispatch behavior.
src/IMessage/Sources/IMessageTests/Eventually.swift Adds an async polling helper used by new tests.
src/IMessage/Sources/IMessageTests/DatabaseTickWaitTests.swift Removes a local eventually helper in favor of the new shared one.
src/IMessage/Sources/IMessageTests/AsyncUtilityTests.swift Adds tests for Task.withTimeout, KVO wait helpers, and deep link timeout behavior.
src/IMessage/Sources/IMessageNode/PlatformAPINodeWrapper.swift Updates Node method signature for onThreadSelected to accept nullable thread IDs.
src/IMessage/Sources/IMessageNode/IMessageNodeExports.swift Ensures SystemSettingsOnboarding start/stop execute on MainActor.
src/IMessage/Sources/IMessageCore/Retry.swift Adds async retry(withTimeout:) overload.
src/IMessage/Sources/IMessageCore/Result+Async.swift Adds async Result(catching:) initializer.
src/IMessage/Sources/IMessageCore/PassivelyAwareDispatchQueue.swift Removes legacy idle-aware dispatch queue implementation.
src/IMessage/Sources/IMessageCLI/IMessageCLI.swift Marks authorization prompting as @MainActor.
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/WindowCoordinator.swift Updates window coordinator protocol for async operations and main-actor boundaries.
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/SpacesWindowCoordinator.swift Updates coordinator to async methods and safe @MainActor app access.
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/EclipsingWindowCoordinator.swift Refactors coordinator for async + main-actor isolation of AppKit reads and sendable anchor data.
src/IMessage/Sources/IMessage/WindowCoordination/WindowCoordinators/Eclipsing/EclipsingDebugger.swift Annotates NSScreen helper with @MainActor.
src/IMessage/Sources/IMessage/Utilities/Task+Timeout.swift Adds a cancellation-aware timeout wrapper utility for async operations.
src/IMessage/Sources/IMessage/Utilities/KVO+AsyncStream.swift Adds KVO async streams and async-sequence “first until deadline” helper.
src/IMessage/Sources/IMessage/SystemSettingsOnboarding.swift Marks onboarding API as @MainActor.
src/IMessage/Sources/IMessage/PromptAutomation.swift Converts notifications automation to async NSWorkspace open + async retry/sleep.
src/IMessage/Sources/IMessage/PlatformAPI+MessagesController.swift Replaces queue-based serialization with lane actor; bounds invalidation loops; improves disposal behavior.
src/IMessage/Sources/IMessage/PlatformAPI.swift Converts controller calls to async/await; refactors thread-activity selection observation state.
src/IMessage/Sources/IMessage/Pasteboard+Backup.swift Makes pasteboard restoration helper async-compatible.
src/IMessage/Sources/IMessage/OnboardingManager.swift Moves onboarding window/timer handling onto @MainActor and reduces cross-thread hopping.
src/IMessage/Sources/IMessage/MessagesControllerAutomationLane.swift Adds the new serial automation lane actor implementation with idle callbacks.
src/IMessage/Sources/IMessage/Messages/MessagesInstanceTarget.swift Converts secondary Messages launch to async with timeout and async waitForLaunch.
src/IMessage/Sources/IMessage/Messages/MessagesDeepLink.swift Reworks deep link model from enum to struct with target-thread metadata.
src/IMessage/Sources/IMessage/Messages/MessagesController+PlatformOperations.swift Converts platform operation wrappers to async.
src/IMessage/Sources/IMessage/Messages/MessagesController.swift Major async conversion; introduces withAutomation scoping; serializes user-activation paths through the lane.
src/IMessage/Sources/IMessage/Messages/MessagesAppElements.swift Converts element lookup paths to async and updates deep-link open callback type.
src/IMessage/Sources/IMessage/MacPermissions.swift Forces automation permission prompt onto MainActor.
src/IMessage/Sources/IMessage/KeyPresser.swift Refactors KeyPresser for testability via injection; changes default thread behavior.
src/IMessage/Sources/IMessage/IMessageHost.swift Updates notifications-disable flow to await async PromptAutomation.
src/IMessage/Sources/IMessage/Extensions.swift Replaces sync waitForLaunch with async + timeout and KVO-based waiting; adds @MainActor rect helper.
src/IMessage/Sources/IMessage/Defaults.swift Updates documentation comment for watchThreadActivity.
src/IMessage/Sources/IMDatabaseTestBench/TestBench.swift Removes legacy idle-aware queue testbench command.
src/IMessage/lib/index.ts Updates TypeScript type for onThreadSelected to allow null.
src/api.ts Adjusts runtime behavior to allow null thread selection to flow through (no early-return).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +85
res = .failure(error)
do {
try await onError?(attempt, error)
attempt += 1
} catch let error as CancellationError {
throw error
} catch {
Log.errors.error("retry onError errored \(error)")
}
}
if let interval {
try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
}
Comment on lines +26 to +28
} catch is CancellationError {
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants