savepoint ring slot reclaim + staging sizing -- arbicity/arbi-serve#2296
========================================================================

WHAT IS MEASURED HERE, AND WHAT IS NOT
--------------------------------------
Measured: the hand-out arithmetic, through the SHIPPED ring, store and
snapshotter. Reproducer:

  docker run --rm -e CUDA_VISIBLE_DEVICES="" -v "$PWD:/w" -w /w \
    --entrypoint /opt/venv/bin/python \
    registry.arbi.work/arbi-serve:test-4a2f15c \
    tools/savepoint/ring_handout_replay.py [--sessions N ...]

Only the pool's device half is doubled there -- the three lines that turn a
reclaim hint into a slot, identical to
RecurrentLifecycleMixin.snapshot_recurrent_row. The supersession bookkeeping,
the store and the generation contract are the shipped ones.

STATED, not measured, in the table below: the TRAFFIC -- how many chunk
crossings and reply pages a turn makes. Those come from the 8-conversation
serve run on #2299 (4.5 hand-outs per request, overwrite 115 against 64
first-use), and the shape "3 prefill writes + 6 decode pages" is that run's
write pattern, not a new reading.

NOT measured here at all: the serve-time effect on cached% / TTFT / resume
rate. No card was taken for this change; box 206 was in use by peer agents.
The counter to read on the first serve run after it lands is
savepoint_ring_snapshots{event=...}, which this change splits three ways so
"reclaim" is readable beside "first_use" and "overwrite".


HAND-OUTS PER TURN
------------------
"before" = every write takes a fresh hand-out. "after" = a write reuses the
slot of the entry it just superseded. SAME 64-slot ring in both arms, so the
only difference is the hand-out policy. "overwrite" is snapshots lost to the
ring's bound during the run. Run with --slots-after 64.

  8 conversations, 3 prefill writes + 6 decode pages = 9 writes/turn
  arm         slots  HANDOUTS/turn  overwrite  reclaim
  before         64            9.0          8        0
  after          64            2.0          0       56

  8 conversations, 1 prefill write + 3 decode pages = 4 writes/turn
  before         64            4.0          0        0
  after          64            2.0          0       16

  8 conversations, 5 prefill writes + 40 decode pages = 45 writes/turn
  before         64           45.0        296        0
  after          64            2.0          0      344

  32 conversations, 3 prefill writes + 20 decode pages = 23 writes/turn
  before         64           23.0        672        0
  after          64            2.0          0      672

Hand-outs per turn is 2 in EVERY row -- one rolling prefill slot that ends as
the prompt-end entry, one rolling decode slot -- and does not move with the
reply's length. That is the property that took savepoint_writes_per_turn off
the operator: the number is a function of the write POLICY (2 with the decode
write on, 1 without), not of traffic, so it is derived at boot.

The last two rows are what it is worth: 32 conversations against a 64-slot ring
lost 672 snapshots to the bound before, and none after; 8 conversations with
long replies lost 296, and none after. An entry's retention in TURNS rises by
the ratio of the two hand-out rates -- 4.5x, 11.5x and 22.5x in those rows.


SIZING
------
Slot size is the model's recurrent geometry: 154,927,104 B = 147.75 MiB for
the 27B (measured, #2299 -- 144.00 MiB of it GDN state, 3.75 MiB conv).

The slot COUNT is unchanged on the default box and deliberately so. It resolves
as max(staging, retention, store entry floor):

  staging    max_batch x live slots per turn         8 x 2  = 16
  retention  x2 rounds (peers' round, then its own)          = 32
  floor      savepoint_min_entries                           = 64   <- binds
                                                                64 slots
                                                            = 9.23 GiB pinned

What changed is that "live slots per turn" is now DERIVED (2 with the decode
write on, 1 without) instead of an operator's statement about traffic, which is
what slot reclaim bought. Before this change the same expression read
max(2 x 8 x 1, 64) = 64 with a boot WARNING that 1 was wrong and that
savepoint_writes_per_turn had to be raised from the observed rate. The warning
is gone because the number is no longer a guess.

WHY THE RING IS NOT SHRUNK TO STAGING SIZE HERE
An admitted entry's ``tensors`` are VIEWS into the ring slot the snapshot
landed in, so while that is true the ring IS the retention store and dropping
its slot count does not save memory -- it silently drops retention. The saving
belongs to the chunk-streaming mover (#2302, owned by feat/tier-store), which
carries a snapshot device -> pinned 4 MiB chunk -> destination and never has
the whole blob resident:

  today, retention held in pinned (64 slots)     9,456 MiB
  slot-sized staging, 8 slots                    1,182 MiB
  chunk-streamed, 8 x 4 MiB                         32 MiB
  chunk-streamed, 16 buffers both directions        64 MiB

So the staging and retention terms are named SEPARATELY in
resolve_savepoint_ring_slots, and taking the retention term and the store's
entry floor out is one deletion, licensed by exactly one condition: the store
owning its entries' bytes. That is stated in the function's docstring and
pinned by tests/test_savepoint_fold_emit.py.


SHRINK, DO NOT REFUSE
---------------------
plan_savepoint_ring now floors at one slot instead of returning 0 and falling
back to the per-boundary arena, following loader.flat_dump_ring. The argument
is the same and it is not a preference: the fallback page-locks a whole slot's
worth per snapshot, on the request path, and books none of it on the host
ledger -- strictly worse on the axis the refusal was protecting. A host that
cannot spare one slot is still caught, one step later, where
reserve_pinned_host refuses the booking and names the bytes.

Zero slots now means exactly one thing: the pool holds no recurrent state.


SAFETY
------
reclaim bumps the slot's generation exactly as take does, so the superseded
entry's stamp is dead the instant reclaim returns: is_live is false, the store
drops it on the next probe and answers MISS. A stale stamp still reads as a
miss, never as somebody else's state.

What it gives up, named: a superseded entry is a RANK not a delete, so it
would have gone on serving a regeneration or a branch from mid-reply until
eviction. Once its bytes are reused it cannot. The entry it is traded for is
the deeper one, which serves the linear continuation every ordinary next turn
asks for.

Ordering against a restore already in flight is a device guarantee, not an
assumption: the copies that fill a slot are issued on the spill stream, which
first waits on an event recorded on the COMPUTE stream
(SideStreamSavepointSpill.spill), and a restore reading that slot was issued
on the compute stream at the start of a step
(flush_pending_savepoint_resumes). The read is ordered before the overwrite.

Tests: tests/test_savepoint_ring.py -- the reclaim contract (reuse, refusal on
a stale stamp, refusal on a released hand-out, the two raising shapes), and
the admission seam (a decode write takes the decode entry's slot and never the
prompt-end one; a prompt-end write reuses the prefill slot and drops the
decode hint; the hint does not outlive its request).

Full CPU suite in registry.arbi.work/arbi-serve:test-4a2f15c: 19030 passed,
386 skipped.
