Pin tasks across restore waiter handoff (#98728)
### What?
Keep a task alive across the restore-waiter handoff: from the moment a
caller observes another thread restoring a category until that caller
acquires the restored task for use.
This is the first open layer in stack #98709. The construction and
restore-owner races have already landed in `canary` through #98688 and
#98700.
### Why?
The `restoring` bits elect exactly one thread to perform backing-storage
I/O. A waiting caller observes that ownership under the task lock, drops
the lock, and then holds only a `TaskId` while waiting for notification.
That temporary in-session reference is not represented by a parent or
aggregation edge, so GC may otherwise collect the task after the
restorer clears its bit but before the waiter acquires a guard.
Eviction is different: it is safe for eviction to clear the completed
restore in this window as long as the waiter does not mistake that state
for an I/O failure. The waiter can claim the now-unowned category and
restore it again.
Batch preparation has the same task-id-outside-the-graph interval
between classification and callback. Pair access needs both task IDs
kept alive until both guards can be acquired together.
### How?
Each waiting caller increments the existing `transient_ref_count` before
dropping the task lock and decrements exactly one ref under the guard at
its actual use boundary. This uses the GC-root mechanism and invariant
checks that already protect construction, transient-parent, and
detached-handle references.
The transient ref does not block value eviction. If a waiter observes a
category that is neither restored nor currently restoring, it keeps its
GC pin, claims the category, and performs another restore. A replacement
restorer retains its acquired guard through notification and returns
that same still-pinned guard to the caller.
Successful pin release belongs entirely to the caller:
- single-task access releases after receiving the restored guard;
- pair access releases after acquiring both guards together;
- batch preparation releases immediately before passing the guard to its
callback.
The helper decrements the ref itself only on `Err`, where no guard is
returned. The already-restored fast path remains and avoids listener
registration.
This removes the dedicated handoff field and eviction blocker. It also
keeps `gc_maybe_collectible()` and
`gc_debug_assert_root_held_by_transient_pin()` aligned on the same
existing transient-ref predicate.
### Verification
- Latest `origin/canary` (`389f164d`) in a detached worktree
- `cargo test -p turbo-tasks-backend --lib` (102 passed)
- `cargo test -p turbo-tasks-backend --test eviction --test
gc_resurrection --test gc_collection --test gc_interrupt --test
gc_stress` (19 passed)
- Eviction-permitting stress: 0 failures across 1,200 runs using 8
parallel workers
- Sequential stress: 0 failures across 150 runs
- `cargo fmt -- --check`
- `cargo clippy -p turbo-tasks-backend --tests -- -D warnings`
Followed by #98729.
<!-- NEXT_JS_LLM -->
<!-- fleet 6aa6acec-6286-4b03-86f4-13f4c7e2b2fa -->
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>