ci / web (pull_request) Successful in 1m11s
apple / swift (pull_request) Successful in 1m18s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m24s
ci / rust-arm64 (pull_request) Successful in 1m35s
android / android (pull_request) Successful in 3m11s
ci / rust (pull_request) Successful in 6m3s
Unplug a controller mid-session and the virtual pad it was driving outlives it: the game keeps seeing a connected, permanently idle device for the rest of the session. The single-controller session — the common case — hits this every time. `PadSlots::sweep` needs two passes to retire a pad. The first pass to see the mask bit clear only ARMS the 300 ms devnode-churn grace; the drop lands on a later pass. But sweep runs only from a state frame, and the producer emits exactly one frame per detach — `native/input.rs` guards the emit on the bit still being set — so for a pad with no still-changing sibling in the same manager, the second pass never comes. Nothing periodic reaches sweep: `heartbeat` and `pump` walk the slots without it. Split the two halves. `sweep` still folds a frame's mask into the grace clocks, and `reap` — new — drops whatever has run out, with no frame needed. Every manager now reaps on the periodic pump it already runs, so the teardown completes ~300 ms after the detach instead of never. `reap` deliberately cannot arm a clock: it only reads `inactive_since` and clears it, so a pad whose bit never went clear has nothing to run out and no amount of reaping can drop it. That is what makes it safe on a hot loop, and it keeps the anti-flap guarantee intact — a mask that blips clear and returns still never churns a devnode. The two existing tests hand-fed a SECOND removal frame, which production never sends; they passed while the real path leaked. Both now drive the unplug through a pump tick, and PadSlots gains three tests pinning the new invariants. Verified non-vacuous: with the reap neutered, both manager tests fail with "the pump tick never completed the unplug". Behaviour notes: this puts UI_DEV_DESTROY on the GameStream control thread's budget for the first time, and a mask glitch longer than the grace now really does flap — which is SWEEP_GRACE working as documented, so the constant stays. Found by the 2026-08-03 force-feedback sweep (B2 — see the backlog in punktfunk-planning design/haptics-sweep-2026-08-03.md).