src/switcher/state/TabReadPolicySpecs.md
Decides which tracked windows pay for an OS-tab read (AXTabGroup) on a given switcher show.
AXUIElement.tabGroupInfo looks for an AXTabGroup among a window's direct children. There is no OS
batch across elements, so it costs one Mach round trip per child, and a window with no tabs pays for all
of them before concluding there is no tab bar. An ordinary AppKit window exposes several children (window
buttons, a toolbar, a split group), so reading every tracked window on every show ran to several hundred
round trips per summon, spread across as many processes — each able to stall a bounded worker for the whole
1s messaging timeout.
The per-show read was never the mechanism that keeps tab groups correct. Every real change already announces itself:
| change | signal that already exists |
|---|---|
| tab opened / closed | WindowServer 811 / 804 (a tab is a window) |
| active tab switched | AXMainWindowChanged naming the wid |
| tab renamed | AXTitleChanged → Applications.applyObservedTitle |
| tab dragged out | the reducer's own drag-out re-check |
So the read is a backstop, and this policy sizes it like one.
lastReadGeneration == nil) → read. Nothing is known about this window's tabs.backstopReadsPerPass (5) stalest windows, which are re-read so a
notification that never arrived is corrected within a few summons.Ties in staleness break towards apps that have actually shown a tab group, then by wid so the order is total and the tests are deterministic.
"This app has tabs, so read all its windows every show." Finder and Terminal are precisely the apps with the most windows and the most children per window, and they are also the apps the event signals cover best. Capability only orders the backstop; it never forces a read on its own.
Closing one tab and opening another between two shows leaves the count unchanged while the grouping has
entirely moved. Windows.appWindowSetVersion is bumped on every append and every removal, and
Applications.noteTabRead records the version a read was ISSUED against (not the one current when its
answer lands, which would mark a window up to date with a window set that changed mid-flight).
Applications.refreshWindowTitleAndTabs(_:_:_:reconcileTabs:) already understood reconcileTabs: false: it
drops kAXChildren from the attribute batch and skips the child walk entirely, and when the app holds a live
AXTitleChanged subscription it skips the whole call — because a title-only read is work the
notification has already done. So a skipped window costs either one round trip (title) or zero.
backstopReadsPerPass windows are re-read, stalest first.