src/util/SchedulingPolicySpecs.md
SchedulingPolicy holds the pure timing decisions behind the AX scheduling layer, extracted so they're
testable without real clocks or queues (same pattern as SelectionResolver / AxQueryRouting):
ThrottleDecision — for one throttleOrProceed call: run on the leading edge, or (within the
window) schedule a single trailing run and coalesce the rest. Used by Throttler and ThrottlerWithKey.RetryPolicy — backoff schedule (200ms → 1s → 2s → 5s, then 5s) and the 60s give-up, for retrying
an AX call against an unresponsive app. Used by AXCallScheduler.SurfaceAcquisitionPolicy — may the inventory sweep spend another brute-force acquisition on a
WindowServer surface it has repeatedly failed to find an AX element for?InactiveTabScanPolicy — may we brute-force an app's AX tree for the inactive tabs its AXTabGroup named
but we hold no window for, and WHERE should that sweep start? That walk is the ONLY way to adopt an inactive tab (it appears in no CGS list) and
it is expensive, so it is gated per app on the SITUATION: the untracked titles plus the app's window count,
which is what says whether anything has changed since we last looked. Used by
Applications.discoverInactiveTabs.Mirrors SchedulingPolicyTests.swift 1:1.
runNow.runNow (window reset).scheduleTail(remaining).coalesce.runNow.scheduleTail, then coalesce for the rest.The situation used to be recorded BEFORE the scan ran and then refused forever, so one fruitless attempt — the app's AX tree not ready yet, the classic at launch — permanently gave up on it, with no retry and no later trigger. Measured over a live QA run (2026-07-30): 82 tab reads named untracked tabs and the scan adopted NOTHING, against 57 adoptions in a run whose first attempt happened to land. So the outcome is what gets recorded, and a situation gets a small budget instead of exactly one shot.
testFinderTabsAllUntracked) and no number of walks resolves them. Past the cap the tree is left alone.0..<30000 (~9.7k per 250ms) and adopted nothing, while Finder's window
elements sat at ~31000 — stopping just short, every time. A tab's window element is minted when the tab is,
so an app's windows cluster in a narrow band and a window we already track names it; anchoring a margin
below found them in a single attempt (the live cases went green, the slowest from 34s to 16s).Sibling of InactiveTabScanPolicy: the same bounded, situation-keyed budget over the OTHER brute-force.
The inventory sweep acquires AX elements for WindowServer surfaces it holds none for. Other-Space acquisition uses a remote-token sweep capped by WALL CLOCK; eligible surfaces of one process now share that 250ms traversal, but an unresolved process set still does not fail fast and must have a finite retry budget.
Measured live before process batching, 2026-08-28, on a 4-window desktop. Twelve such surfaces existed (Dock, Spotlight, Control Center, WallpaperAgent, BetterDisplay, CopyQ, a Chrome surface, PAH_Extension); none was a window, yet each started an identical per-wid traversal. Six at a time on the 6-wide scan pool, that was ~550ms per show and 82,167 of its 82,221 AX round trips. Process batching removes that multiplier; this policy still bounds repeated batches whose unresolved members remain unchanged.
Only the periodic sweep is gated. A surface that changes state reaches Applications.discoverWindow on
its own event, and that path uses the cheap kAXWindows route with no brute-force, so refusing the sweep
cannot make a window undiscoverable.