Implement the partial B3 staging-session slice
Scope 6.5 deliverables 1-5: what a staging session is on disk, what it costs, when it dies. The instance-layer half of plan §8 — identity, policy, ProjectionCore, the v2 routes — is deliberately absent; B3 binds and exposes the fields those checks key on and evaluates none of them. Two findings from the B2 review shaped the result more than the original deliverables did. Restart was in-memory handle reuse. open() built a fresh registry and never read the filesystem, so after a real reopen an existing session ID was admitted as new -- and if materialization then found the old directory, error cleanup could unlink a durable session. That is data loss reachable from an ordinary restart plus one error. open() now scans, validates, and reconstructs sessions, chunk indexes, and the whole occupancy account before returning, so a reconstructed ID is occupied and refused as a conflict long before the error path; and that path no longer calls remove_dir_all. Either change alone closes the loss. A session is final iff its directory holds a valid session record for its own ID, installed by rename_noreplace over fenced, digest-checked bytes, so the final name only ever appears atomically over complete data. A directory without one is abandoned materialization -- a crash between mkdir and that rename, unaccounted and unreferenceable. The two states share no code path, and reclaimed abandonments count on their own counter so they can never be read as aborts or expiries. Global quotas were per-handle. Every open() built an independent registry outside the root LOCK, so two handles admitted twice the global limit and the atomic-insertion work bought nothing across them. Construction now requires proof of the held root lock and refuses a second in-process instance, making two accountants on one root inexpressible rather than discouraged. Also: bounds are enforced atomically with insertion under one mutex with no read-then-decide path, refused as typed LimitExceeded or Overloaded and never by eviction; the directory-sync test pinned two syncs when the first session in a shard needs three, a counter assertion that encoded the bug; cleanup now validates a whole directory before unlinking anything, rather than discovering a surprise midway through destroying a live session; and artifact I/O moved off the registry mutex onto maintenance workers, with the calling thread asserted to hold no guard rather than documented not to. Deliverables 6-8 are explicit NotImplemented naming themselves. StagedSessionState omits Finalizing, so deliverable 6 will fail to compile at exactly the expiry and abort sites that must learn about a pin. Carry-forwards recorded in §6.5, not closed: no production path begins a session, so the sealed-invisibility acceptance stays ignored with both blockers named; and expire() has no scheduler, so session age is a bound enforced when asked and never asked. 116 library tests, 27 staging tests, 1 intentionally ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef7d263fed
commit
4c68f08446