cmux-next: FolderChanges.events() returns once its watches are registered

Merged
TE
9d ago
+91-18

Fix-forward for #26320 (squash 39d216bc), from its post-landing review, which answered FIX:

The startup race remains — FolderChanges.swift:44,87. arm() calls source.resume(), which does not guarantee registration has completed. dispatch_source(3) explicitly documents that events during this delay are missed. A write after events() returns can therefore still disappear. Establish readiness through setRegistrationHandler before promising the watch is armed; rearming needs the same guarantee.

The test remains timing-dependent — FolderChangesTests.swift:27–32. [...] Add a deterministic readiness test with delayed registration.

What changes

  • FolderChanges.events() is now async. arm() gives every dispatch source a registration handler and waits for each one before it returns, so a change made after events() returns is always delivered. The wait ends early if the calling task is cancelled, so a cancelled caller never hangs.
  • When the watch has to move, the loop waits for the new watches to register before it cancels the old ones and yields the element, so re-arming has the same guarantee.
  • BackdropCatalogStore (the only production caller) awaits events().
  • If the caller is cancelled during that wait, events() cancels the watch and returns a finished stream. The detached loop never holds descriptors for a caller that will not iterate.
  • New test FolderChangesTests.eventsReturnsOnlyOnceTheWatchesAreRegistered. An internal init sets the queue the sources target. The test holds that queue suspended so no registration handler can run, and starts events() with Task.immediate, so it runs synchronously up to its first suspension. It checks two things: events() returns only after the queue is resumed (from an ordered step log), and a write made the moment it returns produces an element. There is no sleep. With the fix the test cannot fail, however the threads are scheduled. Without the wait, events() has no suspension point and returns before the test resumes the queue, so the test fails every time.
  • Kept from the earlier review: there are no new file descriptors or sources. Cancel handlers still close every fd, and cancellation still reaches armed.cancel().

No capture: no visible UI change; this only changes when the folder watch behind the wallpaper catalog reports that it is ready.

Risk

Low. The change is contained to the folder watch behind the wallpaper catalog. events() now waits for kernel registration before returning, which is sub-millisecond.

Verification

Fleet runs pending; this section is updated with their results.

Agent session: agent session

šŸ¤– Generated with Claude Code