Hand-woven concurrency gates cannot come back

A SemaphoreSlim, ManualResetEventSlim, AutoResetEvent or CountdownEvent parks a thread. On this mesh that thread is usually a single-threaded action block or a grain turn, so the message the gate is waiting for can never be processed — a deadlock nothing recovers from.

Serialization already belongs to the hub, and concurrency bounding already belongs to the I/O pool. A new guard makes that enforceable rather than merely written down.

Two tiers

Why the test tree counts too

In a test the same primitive fails more subtly: it strands a blocked worker when an assertion throws before the release runs. Measured this week — a pool test put its release after an await of the method-timeout token, so on timeout the release never ran and a blocked leaf held a pool thread into the next test. The replacement is an observable the producer completes, released in a finally.

Proven by breaking it

The guard's self-test plants a real directory tree and runs the real scan over it, rather than matching its pattern against strings — a guard whose self-test never calls its own scanner can lose half its scope and stay green. Each arm was then verified by injecting a violation and watching it go red: a new gate in src/, a new file in test/, and a listed file whose count grows.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.