# The hot-plug matrix: unplug anything, replug anywhere *Status: COMPLETE, 2026-08-09, same day — H1..H5 all green, full suite 124/124. The predicted cascade bug was real and found by H2: `tearDownPort` did not recurse into a hub yanked from a root port, so its subtree stayed live against vanished hardware and the replugged hub found its port occupied; fixed by mirroring `tearDownHubDevice`'s children-first recursion (77e7001). Every other cell passed against the establishment-track machinery unchanged.* *2026-08-09. The requirement, verbatim: "i want to be able to unplug devices and plug them back in, in any order and on any hub." The machinery exists end to end — per-device driver processes, cascading teardown (`tearDownHubDevice` recurses a departing hub's subtree, children first), reap-on-removal in the manager, idempotent per-port registration identity, lineage rebinding — but only one cell of the matrix is verified: a leaf keyboard behind a hub, unplugged and replugged on the same port (`usb-hub-unplug`). This plan verifies the rest and fixes what verification flushes out. The cascade path has never executed; expect it to carry at least one bug.* **Out of scope, stated up front:** unplugging the BOOT stick (the drivers handle it; fat holds a dead block channel until M21 remount gives it re-acquisition), and real-hardware root-port timing (the Ryzen bench is the acceptance run for that, as always — QEMU proves the logic, not the silicon). ## The discrimination lever Every case's rebind tail (— reaping → delegated → second-generation `ok`) is impossible without the manager's reap: stashing the reap out of `onChildRemoved`/`pruneChildrenOf` makes every matrix case fail at the dedupe wall. That is the standing discrimination check for the whole matrix — run it once per case shape, not per commit. ## The matrix | Case | Topology | Event sequence | What it proves | |---|---|---|---| | H1 `usb-root-replug` | keyboard on a 2nd controller's ROOT port | del @8s, add @14s (same port) | the `tearDownPort` path + reap + rebind; root-port changes arrive via the 250 ms reconcile tick (QEMU raises no root-port events) | | H2 `usb-hub-yank` | hub on 2nd controller; keyboard AND mouse behind it | del the HUB @8s; re-add hub @14s, kbd @17s, mouse @18s | the recursive cascade: one event removes the subtree, EVERY bound driver is reaped, and the rebuilt hub re-binds both | | H3 `usb-hub-nested-yank` | hub → hub → keyboard | del the OUTER hub @8s; re-add all three @14–18s | the cascade recursion depth ≥ 2, and a nested rebuild | | H4 `usb-replug-moved` | keyboard behind hub port 1.1 | del @8s; add on port 1.2 @14s | a replug on a DIFFERENT port is simply a new device: new port identity, new id, fresh match — no stale state ties a driver to the old port | | H5 `usb-replug-cycles` | keyboard behind hub | del/add three times (6 hooks) | no state leaks across generations: slots, the bus's open table, driver entries (reaped entries must be reusable) | Ordered expects follow the `usb-hub-unplug` shape: the boot devices' `ok` lines all precede the first unplug, so a tail of `removed → reaping → delegated → ok` can only be satisfied by the generation the sequence created. H2/H3 require one `reaping` per bound child; H5 requires three. ## Order and discipline H1 → H2 → H3 → H4 → H5, one commit per case (plus its fix if it finds a bug — the case and the fix land together, the case having failed first). The standing discrimination run once for H1 and once for H2's shape. Full suite green at the end, docs touched where behavior was corrected, memory updated. Fixes stay within the settled design: teardown order (children first), reap-on-removal, identity per port — anything design-shaped that surfaces stops the loop and asks.