Garbage Collection and Forgetting

Stores grow as signed commits, derived artifacts, authorization evidence, and user blobs accumulate. Because every blob is content addressed and immutable, nothing is overwritten in place. A node periodically forgets local bytes which no retained record or explicit policy root owns.

Forgetting is deliberately conservative. It removes only a local copy, so later synchronization or an explicit fetch may restore it. An assertion is not semantically retracted merely because one node drops physical bytes.

Structural Ownership

Lifetime is a property of stored structure, not semantic trust. Every retained current native record other than a BLOB strongly owns each blob handle it names directly:

RecordDirect blob references
COMMITcollection descriptor, data, metadata
MERGEcollection descriptor, low input, high input, result
DERIVEtarget descriptor, input, output
authorization proofeach capability-definition handle in its signed path
WANT Blob(H)H

Each direct reference is an independent recursive ownership edge. If the referenced blob is resident, collection retains it and every resident child found by the ordinary conservative blob walk. If it is absent, collection does not fetch it, manufacture a subordinate WANT, fail, or withhold any resident sibling. A blob record does not own itself merely because its bytes occur in a store; it survives only when some retained record, legacy strong pin, or explicit policy root reaches it.

This rule deliberately does not verify a commit or proof signature, perform collection admission, or decide whether an equation is algebraically useful. Those are semantic questions for a reader. Physical preservation must remain stable when later evidence changes what a record means, and it must not let an authorization bug silently destroy bytes. Conversely, retaining bytes grants no authority. A retained proof owns its resident capability-definition closure, but its opaque resource bytes do not become a lifetime edge even when an application interprets them as a collection descriptor handle.

Witness-bound MERGE and DERIVE also name actual native input records through record_references(). Those fingerprints are record identities, not blob handles: do not feed them into the conservative blob walker or the DHT. The retained native ledger preserves the referenced records independently of whether their payloads are resident. Exact witness closure remains necessary to recover Support; eviction must not silently replace a missing record with another record producing the same payload.

The typed reference enumeration lives on the records themselves. MemoryRepo, Pile rewrites, and Yard collection all consume the same enumeration rather than maintaining backend-specific interpretations of record fields.

Explicit Policy Roots

RetentionRoots supplements structural record ownership for callers with local policy. It distinguishes:

  • a direct root, which retains exactly the named blob; and
  • a recursive root, which retains the blob and every resident descendant found by conservative traversal.

Explicit roots may describe application policy which is not represented by a native record. Unlike an absent structural reference, an explicitly requested blob missing during a retained Pile rewrite remains a transfer error: the caller asked for that exact copy and silently weakening the request would be incorrect.

There is no collection-specific retention planner. In particular, admission results do not select lifetime roots and MERGE or DERIVE records are not treated as weaker cache hints. A retained MERGE owns its resident descriptor, result, and inputs, and a retained DERIVE its resident target descriptor and output, exactly as a retained commit owns its resident descriptor, data, and metadata. A DERIVE's source locator is a one-way image of a handle, not a handle, so it owns nothing. Naming the descriptor matters most for a derived collection: it holds no COMMIT, so its MERGEs and DERIVEs are the only records that keep the descriptor every one of its records needs to be decided.

This ownership rule is deliberately stronger than read-time dependency selection. An authorized witness-bound output can be read without historical input payloads or metadata; nevertheless, if those blobs are already resident, the retained equation or COMMIT still owns them. Cheaper attachment is not an implicit permission to discard historical bytes or records.

Known retired unsigned equations remain inert in semantic readers, but their direct references still retain resident blobs. Reframing preserves their exact bytes. Unknown opaque kinds cannot safely define a smaller live set because their ownership edges are unknown. A retained Pile rewrite instead preserves their bytes and every resident blob; semantic reframe and Yard reclamation still refuse them.

The retired equation kinds -- the unsigned forms and the later witness-bound signed forms -- are opaque to ordinary replay. Retained Pile copying carries their frames and all resident blobs under the same opaque rule; Yard and semantic reframe refuse them. No shipped command restates them any more: the migration that once re-signed them under a current writer's key was applied to the live piles and then removed with the record kinds it produced. Ordinary GC never deletes the historical frames.

Backend Boundaries

MemoryRepo::keep, Pile::rewrite_retained_into, and Yard collection apply the same law:

  1. freeze one coherent resident view;
  2. enumerate every retained native record;
  3. add each independently resident direct reference as a recursive root;
  4. add caller-selected direct and recursive policy roots;
  5. recursively mark resident candidate handles; and
  6. rewrite or evict everything outside that conservative live set while preserving the retained record ledger.

Pile rewriting exposes an explicit choice to preserve or drop WANT records. Preserving a WANT preserves its ownership edges; dropping it omits the record and therefore those edges. Yard's grow-only WANT set has no recency or byte budget: while a WANT record is retained, its resident references are ordinary strong roots. Physical removal belongs at an explicit rewrite/eviction boundary rather than to a hidden in-memory weakening of the record.

For migration safety, a retained Pile rewrite also recreates the exact immutable legacy strong-pin snapshot it observed. A resident pin head remains a recursive root; a dangling pin remains dangling. Legacy collection headers whose old identities cannot express the current algebra are inert evidence and own no current blobs.

Opaque records form a harder boundary. Their span may be known while their ownership semantics are not. Pile::rewrite_retained_into carries each byte-distinct opaque frame unchanged and widens retention to every blob in its observed resident snapshot, even when the caller selected fewer roots. This allows trible pile compact to remove redundant copies without interpreting or discarding unknown ownership. Repeating a copy does not append an opaque frame whose BLAKE3 digest is already present; this is set preservation, not preservation of physical offsets or duplicate occurrences.

Yard collection, compaction, and reclaim still refuse before changing physical state when an opaque record is present. They cannot infer a safe eviction set from unknown ownership. Semantic reframe likewise retains its unknown-kind preflight refusal; it is a different operation from retained Pile copying.

A retained rewrite can also leave collection-algebra frames behind. PileFile::rewrite_retained_into_filtered asks a CollectionFrameFilter about every COMMIT, MERGE and DERIVE frame it would carry, in every encoding the pile still recognises, before drained generations and supersession apply. A frame a concatenated pile holds twice is considered once. Its retention follows what it carries: a frame left behind roots nothing, a carried legacy V3 header roots the blob handles its fields name, and a carried frame of unknown kind or retired capability proof roots the resident blobs any 32 bytes of it name, at any offset (frames have not always kept their fields on 32-byte boundaries), rather than widening to every resident blob. Without explicit roots it therefore keeps exactly what the carried records, headers, proofs, WANTs and pins reach. A blob the walk reaches of which no occurrence matches its hash has no valid bytes to copy. The walk finds one wherever it meets it, and by default the rewrite then refuses before writing anything, as an invalid explicitly selected blob fails the plain rewrite. A caller may set such blobs aside instead: they are listed, not copied, and their damaged bytes are still read for what they name, but a valid blob named only by a damaged word cannot be reached and is left behind. PileFile::plan_retained_rewrite runs the same selection pass and retention walk without writing, so a dry run reports exactly what the rewrite will do. PileFile::prepare_retained_rewrite retains that decided selection and its immutable source snapshot for a subsequent consume-once write_into, without evaluating a stateful filter or walking the blob closure again. Later source appends are outside that prefix. Whole-file tools must still quiesce writers and check source length; this is a frozen observation, not a reusable recipe. The lattice v3 cutover (trible pile migrate SRC lattice-v3) is this rewrite with only the roots its operator names: it drops every MERGE, since each host folds and rebuilds only its own, the DERIVEs of mappings that became attached indexes or were deleted, and every blob only those reached, including the descriptor of a collection no kept record names unless a root keeps it.

trible pile compact SRC --into DST --drop-collection NAME_OR_FULL_HANDLE is another explicit filtered rewrite. Repeated selections leave the named collections' frames behind and close over resident descriptor sources, attached parents and sibling-index dependencies. Missing or ambiguous names refuse before creating the destination. Full handles select exact identities referenced by collection records or present as resident tagged descriptors; absent handles refuse as well. A descriptor needs no name or admitted member. Unreadable descriptors remain conservative: the command does not invent their ancestry. Selected mode opts into reachability GC, so raw blobs with no carried record, proof, WANT, pin or opaque-frame reference are not promised retention. Shared payloads and metadata reached by retained collections remain. The native planner runs before destination creation and reached corrupt blobs refuse. Without this option, compact stays conservative and roots every resident blob. --drop-drained still applies its existing validation when combined. Neither mode retracts distributed assertions or revokes authority; normal synchronization can restore excluded records to a later replica. Retained proof evidence can still name an excluded collection.

Conservative Reachability

Canonical archives contain fixed 64-byte tribles whose value half is one 32-byte inline value. The generic walker treats aligned 32-byte chunks as candidate handles and follows those which name resident blobs. This may keep an accidental extra blob, but it cannot plausibly discard a real reference. Opaque attachments normally behave as leaves because their chunks do not name another resident object.

This asymmetry is the safety rule: uncertainty keeps resident bytes. Missing bytes remain missing without turning collection into acquisition, and no semantic rejection is allowed to erase structural ownership.

Operational Tips

  • Schedule forgetting after meaningful imports or merges so one walk amortizes many writes.
  • Budget headroom for synchronization, which may restore locally forgotten copies.
  • When application policy is uncertain, add an explicit recursive root; conservative extra retention costs space, while an omitted true root costs recoverability.