The sealing fallback was reaching a journal that seals nothing. A recovery
keeping an empty tail gave it the fallback's generation, but no TailRange
recorded the move — nothing was sealed — so the next open derived the identity
the manifest still implied while the session in between had appended frames
under the moved one and sealed a run over them. The guard searched published
runs for the derived generation, found none, and let the store open with the
run unresolvable. Review confirmed it: empty tail plus an orphan at 1, reopen
taking 2, frames and a run at 2, an orphan at 2, then an open at 3 with
`object_source(0, 2) == None`.
`RecoveryGenerations` carries a third number. `tail` is what a surviving
journal keeps and is always the tail's own identity: segment-name occupancy is
a fact about `segments/`, and a journal that seals nothing does not go there.
`logical` remains the sealing identity and still falls back under the guard.
That restores the invariant the split rests on — the active tail's identity is
always derivable from the manifest — so the guard and the frames are always
naming the same generation. A tail may keep a generation an orphan occupies;
the collision is only real when frames are sealed under that name, and it is
refused then, with the run in hand.
The regression follows the reported sequence and reads the run's generation out
of the run rather than assuming it, since which identity the frames ended up
with is the thing under test. Mutation-checked in both places it can fail: the
run drifts to generation 2, and with the mid-assertion relaxed the open
succeeds with None pinned there, reproducing the report exactly. The shared
helper now reports the run's own generation, so all three refusal paths fail
legibly rather than against a hard-coded number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh