# Reclaiming Storage

Version control for data is not free the way it is for code. A pin is a native ref, and a native ref holds storage: an Icechunk tag keeps every chunk its snapshot references, a Neon branch keeps its history window, and an Iceberg tag blocks snapshot expiry. Each commit adds one pin per changed object, and each fork adds a branch. This page is the home of two things: **plans**, which preview store writes before they happen, and the three levers that keep the storage bill in check.

The rule underneath: **tether's VCS history is the reference set.** A pin, branch or listing stays alive while some manifest in some commit names it. Drop the commit, and `gc` releases what it held.


# Preview before you write: plans

Most commands that write to a store are a read-only *plan* followed by an *apply* ([the list](cli.md#plans)):

``` bash
tether commit -m "Relabel plate1" --dry-run
```

    plan: commit (3 write(s))
                     pin  zarr/imaging  [icechunk] tether.0a1b2c3d.deb4861eb388e5f6  native ref at snapshot_id=K3F9M2A1B7C8D4E5…
                  record  features      [lance] pin=record: state branch=main version=7, no native ref
                  record  raw/plate1    [file] Observed: recorded, not recoverable
              vcs-commit  jj commit: 'Relabel plate1'
                          db/metrics: unchanged

Nothing happened: no tag, no manifest write, no VCS commit. `--json` prints the same plan as data, and `--plan FILE` saves it for later:

``` bash
tether commit -m "Relabel plate1" --plan /tmp/commit.json   # plan only
# review, get sign-off, ...
tether commit --from-plan /tmp/commit.json                   # apply it
```

Apply checks the world again first. If an object moved, or the manifests changed, the apply is refused with [StalePlanError](../reference/StalePlanError.md#tether.StalePlanError) and nothing is written: plan again and look. `gc` and `drop` are dry runs by default, and need `--no-dry-run` (or `--from-plan`) to apply.

From Python the split is explicit (`repo.plan_commit(...)` then `repo.apply_commit(plan)`, and so on), and [Plan.to_json()](../reference/Plan.to_json.md#tether.Plan.to_json) and [Plan.from_json()](../reference/Plan.from_json.md#tether.Plan.from_json) round-trip. The convenience forms (`tether commit`, `Repo.new()`, …) plan and apply under one lock, with the same checks.


## What a plan checks before it applies

A plan records what it saw as typed [preconditions](../reference/upgrade.Plan.md#tether.upgrade.Plan.preconditions) ([Plan.preconditions](../reference/upgrade.Plan.md#tether.upgrade.Plan.preconditions), visible in a saved plan's JSON). Every `apply_*` runs them all before its first action, and the first mismatch stops the whole apply with [StalePlanError](../reference/StalePlanError.md#tether.StalePlanError). So a stale plan does nothing at all.

A saved plan is valid only for the tether version that made it: it records that version ([tether_version](../reference/upgrade.Plan.md#tether.upgrade.Plan.tether_version)), and any other tether refuses to load it ("this plan was made by tether X; this is tether Y: re-run the plan"), as it does every plan saved before plans recorded one ("made by an older tether"). After upgrading `tether-vcs`, plan again.

Each command *requires* some kinds, so a plan that lacks one is refused as stale ("lacks the … precondition(s) tether requires of it"). A saved plan also carries a [digest](../reference/Plan.digest.md#tether.Plan.digest) over its version, actions, context and preconditions. An edited plan fails the digest, so a dropped action or a changed target cannot apply under checks that describe another plan. The digest detects edits; it does not authenticate, since whoever can write the file can recompute it.

| Command | Required preconditions | Per object |
|----|----|----|
| [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) | `workspace_id`, `workspace_bookmark`, [manifest_hash](../reference/manifest_hash.md#tether.manifest_hash) | - |
| [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) | `workspace_id`, [manifest_hash](../reference/manifest_hash.md#tether.manifest_hash) | each [fork](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.fork): `ref_head` (the branch exists) or `ref_absent`; each `reuse`: `ref_head`; each `adopt`: `ref_present` |
| `restore` | `workspace_id`, `workspace_bookmark`, [manifest_hash](../reference/manifest_hash.md#tether.manifest_hash) | each [fork](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.fork): `ref_head` or `ref_absent` |
| [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) | `workspace_id`, `workspace_bookmark`, `bookmark_head`, [manifest_hash](../reference/manifest_hash.md#tether.manifest_hash) | each `fast-forward` or [merge](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.merge): `base_state`, and `ref_head` or `pin_state` |
| `drop` | `workspace_id`, `workspace_bookmark`, `bookmark_head`, `no_new_holders`, `vcs_head`, [history_digest](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.history_digest) | - |
| `gc`, `repair` | `workspace_id`, [history_digest](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.history_digest) | - |

`workspace_id` binds a plan to the checkout that made it (`--plan` first persists the id in `workspace.toml`). `workspace_bookmark` binds it to the bookmark, as both the workspace file and the VCS see it: git's `HEAD` branch, or under jj the bookmarks at `@` (at `@-` when `@` has edits). A `bookmark_head` that names no bookmark fails. Three plans bind to no checkout: `import` checks the manifest hash, [upgrade](../reference/upgrade.upgrade.md#tether.upgrade.upgrade) the dataset's version, and `forget-workspace` nothing.

[history_digest](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.history_digest) covers *every* visible commit, because a bookmark committed in another workspace may reference a pin a gc plan would release. A promote's `base_state` is the base branch's head, and `ref_head` or `pin_state` the state its source names.

An `adopt` moves nothing, so it is not bound to a head. `ref_present` holds while the adopted branch exists and no newer generation of it (`.2`, `.3`) has appeared. When the store could say that a `--shared` adopt builds on the bookmark's pin, the plan also carries `ref_descends`: it holds while the branch still does, and fails if the store can no longer tell.

A race *during* apply is handled per action instead:

- The heads a plan reviewed are what the stores move *from*: a ref that moved in between is refused with [RefMovedError](../reference/errors.RefMovedError.md#tether.errors.RefMovedError), atomically where the backend can ([conditional moves](cli.md#conditional-moves)).
- A branch that moves after gc's preflight is kept and reported, while the rest of the plan finishes.
- [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) fingerprints each object again against one snapshot.
- [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) lands the *state* the plan reviewed, not the source branch's head at apply time, so a write that slips in between is not promoted.
- A lazy fork on the first writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) resets an existing branch only onto the head [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) reviewed and agreed to reset.

Not every store write is a plan. `pull` pins what moved and commits it, `undo` deletes the branches an operation created, `abandon --gc` applies the `gc` plan it prints, `add --create` makes a store, and the first writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) forks the branch [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) decided on. None takes `--dry-run`, but `new --eager --dry-run` previews the forks, and [abandon](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.abandon) without `--gc` shows its plan without applying it.


# Lever 1: do not pin what you will not need - `--pin record`

Forking needs a *source*, not a *pin*. Say you branch off to try something, and only the two heads matter afterwards, not recovering the exact split point next year. Then the tag at the split point is pure cost. Register the object with `--pin record`:

``` bash
tether add features s3://bucket/features.lance --kind lance --pin record
tether add zarr/imaging s3://bucket/imaging --kind icechunk --pin record
```

With `pin = "record"` on any backend:

- [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) records the state (a snapshot id, version, commit hash or LSN) but creates **no native ref**. The plan shows `record ... pin=record`.
- [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) forks **straight from the recorded state**: the snapshot id in Icechunk and Iceberg, `(branch, version)` in Lance, the commit in Dolt and git, and in Neon the LSN on the branch it was read from. It does so at once even when forks are lazy, since that branch is what keeps the state alive, unless you pass `--defer-record` ([Lazy or eager](branching-and-writing.md#lazy-or-eager)).
- [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) checks that the state is still reachable, and `open -r` works while it is.

The trade: recoverability is now bounded by the system's own retention. Icechunk's snapshot lives until someone expires it, Iceberg's until snapshot expiry runs, and Neon's until the history window closes. `verify --all-history` turns each expiry into a [MISSING](../reference/testing.VerifyStatus.md#tether.testing.VerifyStatus.MISSING) report, so you know which old commits have become labels without data. Mix per object: keep `--pin native` on the archive-quality store and use `record` on scratch.


# Lever 2: drop history you no longer want

Say an experiment produced five commits and only the last matters. Each of the four intermediate commits holds pins.

The one-step way is `tether abandon`. It drops the commits in the VCS and shows the `gc` plan for what that freed:

``` bash
tether abandon krlwtmzy ptnwqsyu        # jj change ids, or git shas
```

    abandoned 3c1e9f0a2b7d, 8d2f4a1b9c0e
    plan: gc (2 write(s))
                   unpin  zarr/imaging  [icechunk] tether.0a1b2c3d.7d1e0c9a2b34e5f6  no manifest in history references it
                   unpin  db/metrics    [neon] tether.0a1b2c3d.b81f4c2d9e60a7c3  no manifest in history references it
    (not applied; `tether gc --no-dry-run` releases these, or re-run with --gc)

Later commits keep their manifests exactly as they were: a manifest records a whole state, so removing an earlier one must not conflict with it, as a plain `jj abandon` or `git rebase` would. The abandon is the VCS's to undo (`jj undo`, the reflog); bring the commits back before running `gc`, and nothing was lost.

The VCS's own tools work too, followed by `tether gc`:


- <a href="" id="tabset-1-1-tab" class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabset-1-1" role="tab" aria-controls="tabset-1-1" aria-selected="true">jj</a>
- <a href="" id="tabset-1-2-tab" class="nav-link" data-bs-toggle="tab" data-bs-target="#tabset-1-2" role="tab" aria-controls="tabset-1-2" aria-selected="false">git</a>


``` bash
jj log -r 'main..@'
# ○  vqmxzkwl  Scores for A1, third try
# ○  ptnwqsyu  Scores for A1, second try
# ○  krlwtmzy  Scores for A1, first try
# ○  ntsyulyo  Relabel plate1 with model v3
# ◆  main

# Keep the split point and the result; fold the tries into the result.
jj squash --from krlwtmzy::ptnwqsyu --into vqmxzkwl
```

`jj squash` and `jj abandon` hide the old commits from the history `gc` walks. `jj op log` can restore them for a while, but after a `gc` a restored commit points at pins that are gone, and [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) says so.


``` bash
git log --oneline main..HEAD
# 9e1c3a0 Scores for A1, third try
# 5b77d21 Scores for A1, second try
# c0ffee1 Scores for A1, first try
# a1b2c3d Relabel plate1 with model v3

git rebase -i a1b2c3d      # mark the two middle commits `fixup`/`drop`
```

`gc` walks `rev-list --all`, so anything unreachable from a ref is gone as far as tether is concerned. git's own reflog and `git gc` are independent of that.


To drop a whole side branch, `tether drop NAME` does the VCS half and the store half in one step ([Throwing it away](branching-and-writing.md#throwing-it-away-tether-drop)). History is now three commits, and the pins that only the dropped commits named are orphans.


# Lever 3: `gc`

``` bash
tether verify --all-history --deep   # confirm what is still referenced actually resolves
tether gc                            # dry run: the plan
```

    plan: gc (5 write(s))
                   unpin  zarr/imaging  [icechunk] tether.0a1b2c3d.7d1e0c9a2b34e5f6  no manifest in history references it
                   unpin  zarr/imaging  [icechunk] tether.0a1b2c3d.c48b2f6e1d07a9b0  no manifest in history references it
                   unpin  db/metrics    [neon] tether.0a1b2c3d.b81f4c2d9e60a7c3  no manifest in history references it
                   unpin  db/metrics    [neon] tether.0a1b2c3d.3e5a9d07c1f28b64  no manifest in history references it
          delete-listing                a1c9…e3f2.jsonl  unreferenced

Two tags in Icechunk, two branches in Neon, one content listing. A plain `gc` does three things. It releases the pins nothing references, forgets the working refs of objects `tether remove` took away (their branches stay), and deletes listings no manifest names. It never deletes a branch.

A pin counts as referenced when any of these names it, under any spelling of its store:

- a manifest anywhere in VCS history, every side of a conflicted jj commit included;
- the working tree of this or any other live checkout of the repository;
- an operation still running, or interrupted, in any of them.

Every ref carries the dataset's id (`0a1b2c3d` here), and `gc` only looks inside its own namespace. Had another dataset pinned the same Icechunk repository, the plan would say `zarr/imaging: 2 pin(s) of other datasets left alone`. Manifests of a backend tether no longer ships (`lakefs`) are skipped with a note, and their pins still count as references. Apply it:

``` bash
tether gc --no-dry-run
```

    unpinned 4 pin(s)
      icechunk: 0a1b2c3d.7d1e0c9a2b34e5f6
      icechunk: 0a1b2c3d.c48b2f6e1d07a9b0
      neon: 0a1b2c3d.b81f4c2d9e60a7c3
      neon: 0a1b2c3d.3e5a9d07c1f28b64
    deleted 1 orphan listing(s)

Or save the plan, and apply it from a job with the right credentials:

``` bash
tether gc --plan /tmp/gc.json
tether gc --from-plan /tmp/gc.json
```

When some actions fail, `gc` prints what it applied along with the failures, and exits 3 (1 when nothing was applied). `tether undo` of a `gc` restores the working refs it forgot and the listings it deleted. Released pins and deleted branches stay released. If you restore a commit that references a released pin (`jj undo` of the abandon), `tether repair` recreates the pin from the manifest's state, while the system still has that snapshot. That window is the argument for running `gc` some time *after* a history edit.

Releasing a tag or branch makes the data *eligible* for reclamation; the system decides when. Icechunk needs `repo.expire_snapshots(...)` and then `repo.garbage_collect(...)`, Iceberg `expire_snapshots`, and Lance `cleanup_old_versions`. Neon frees a branch's history on its own schedule. Those are the system's operations, not tether's.

`gc` (and `drop`) refuse while jj reports a conflicted bookmark, or a conflict in `.tether/` that no later commit resolved. A conflicted bookmark has several targets and counts as reaching nothing, and a conflicted manifest is several answers. Resolve it (`jj bookmark set`, `jj resolve`) first. A conflict elsewhere, or one a later commit resolved, is history like any other.


## Pins this clone did not create

`gc` only knows the history this clone has. A teammate's clone commits into the same stores, and until you fetch, its pins are in your store and in none of your commits. So an unreferenced pin is released only when **this clone created it**. Any other gets a `keep-pin` verdict:

    keep-pin  db/metrics    [neon] tether.0a1b2c3d.5e6f7a8b9c0d1e2f  no manifest in history references it; not created by this clone -- another clone's commits may name it (fetch them, or --release-foreign)

`keep-pin` writes nothing, and a plan holding only such lines is empty. Fetching the other clone's bookmarks turns its pins into references. `gc --release-foreign` releases them regardless, for a clone you know is gone or a pin made by hand; the plan marks each one `not created by this clone`.

Which pins are this clone's is recorded in `tether-pinned.jsonl`, beside the repository lock (`.jj/repo/` or `.git/`):

- [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit), `pull` and `repair` add a pin when the store reports the ref as new. A commit that found the pin already there (another clone made it) does not claim it.
- A pin this clone releases is taken off, so another clone creating it again under the same content-addressed id owns it.
- The first `gc` seeds the index from every live checkout's op log, from the progress records of the pins each operation created.
- The index is not in history. A fresh clone, including a re-clone of your own dataset, starts without it, and its `gc` keeps every unreferenced pin as `keep-pin`.


## Branches of bookmarks that are gone

Every bookmark gets a `tether.ws.<dataset>.<bookmark>` branch in each Forkable system it writes to. A bookmark deleted with `jj bookmark delete`, renamed, or left on a wiped machine leaves those branches behind, each holding whatever it wrote. A plain `gc` never deletes a branch, so a forgotten branch is never why data disappears.

`gc --prune-bookmarks` is the sweep. It finds the `tether.ws.*` branches in each system and keeps those of every bookmark the VCS still has, or a live checkout works on. It also keeps any `--keep-bookmark NAME` (a bookmark that exists only on another machine), and every branch a live checkout has as its working ref or pending fork. It judges the rest by their head: gone bookmarks' branches, this bookmark's branches that no current object uses, and legacy per-workspace branches from before bookmarks. This table is the one place the verdicts are defined; `drop` adds [one rule of its own](branching-and-writing.md#throwing-it-away-tether-drop):

| Branch head | Default | `--force-prune` |
|----|----|----|
| natively pinned by some commit (a tag holds it) | `delete-branch` | `delete-branch` |
| equal to the base branch's head (nothing was ever written) | `delete-branch` | `delete-branch` |
| has writes no commit pinned | `keep-branch` | `delete-branch FORCED` (data lost) |
| a `--pin record` state (only this branch keeps it reachable) | `keep-branch` | `delete-branch FORCED` |
| on a backend whose branches *are* the storage ([BRANCH_IS_STORAGE](../reference/testing.Capability.md#tether.testing.Capability.BRANCH_IS_STORAGE): Neon) | `keep-branch` | `delete-branch FORCED` |
| the store refuses to delete it (Neon: pins hang off it) | `keep-branch: cannot be deleted` | the same |

Both sweeps stay inside this dataset's namespace. Pins and branches whose name carries another dataset's id are counted in the plan's notes (`N pin(s) of other datasets left alone`) and never touched, `--force-prune` included.

``` bash
jj bookmark delete relabel                        # the line of work is over
tether gc --prune-bookmarks                       # its branches, everywhere
tether gc --prune-bookmarks --keep-bookmark hpc   # a bookmark that lives only on the cluster
```

    plan: gc (1 write(s))
           delete-branch  zarr/imaging  [icechunk] tether.ws.0a1b2c3d.relabel  bookmark relabel (gone); head is pinned
             keep-branch  features      [lance] tether.ws.0a1b2c3d.relabel  bookmark relabel (gone); kept: has unpinned writes (branch=tether.ws.… version=9) (--force-prune deletes)
             keep-branch  db/metrics    [neon] tether.ws.0a1b2c3d.relabel  bookmark relabel (gone); kept: branch is storage; deleting reclaims its data (--force-prune deletes)
                          keeping live bookmarks: hpc, main

Add `--no-dry-run` to apply. The kept branches are yours to decide. Pin what is on them (`tether new -b NAME` on a commit, then [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit)), or decide the data is disposable and pass `--force-prune`. The plan marks those `FORCED although it has unpinned writes`, so the loss is explicit first:

``` bash
tether gc --prune-bookmarks --force-prune --no-dry-run
```

`tether forget-workspace [ID]` retires a *checkout* and touches no branch: branches belong to bookmarks, which stay live for `gc` while the VCS has them. `tether remove` deletes nothing either; `--prune-bookmarks` later judges the object's branch. (Lance refuses to delete a branch a tag still points at; tether reports it and moves on.)


## Stores tether created

> **Warning: Experimental**
>
> Everything in this section (`add --create`, `Repo.create`, `gc --delete-stores`, `--store`) lives in [tether.experimental.lifecycle](../reference/experimental.lifecycle.md#tether.experimental.lifecycle). It is the one operation with no `repair` path, and has not yet run against an S3 prefix or a second clone with a real remote. The rules err toward keeping. The module's docstring lists what must hold before it graduates.

Everything above releases *refs* inside stores that existed before tether saw them; tether never deletes such a store. A store made with `tether add KEY LOCATOR --kind KIND --create` is different (`repo.add(..., create=True)`; `repo.create(key, kind, locator)` also opens it). tether made it, so tether may take it away once it is plainly garbage: an ephemeral environment creates what it needs, tests, and throws it all away. Two records outside history say what tether made:

- **An owner marker in the store**, naming the dataset: Icechunk repository metadata, `git config tether.owner`, or a `.tether-owner` file. Manifests are untrusted input, so only the marker authorizes a delete.
- **`tether-created.jsonl`**, beside the repository lock and never committed, since an abandoned fork takes with it the manifest that named the store.

`gc --delete-stores` considers these stores last. It is opt-in, because a deleted store has no `repair` and `gc` only knows the history this clone has fetched. A store is planned for `delete-store` only when all five hold:

1.  Its owner marker names this dataset. A marker naming another dataset is noted and left alone; a store with no marker is only dropped from the index (`forget-store`).
2.  No manifest in visible history, in this working tree, or in any live checkout's working tree names it.
3.  Every ref of *this dataset* in it is one this clone can account for. A branch must belong to the bookmark the store was created on, to a live bookmark, or to one a [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) in a live checkout of this clone made. A pin must be one that a [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) or `repair` of this clone recorded. Anything else may be another clone at work in the store, whose commits nothing in your history protects. The plan then touches *nothing* in that store, your own pins included (`keep-store`), and says so.
4.  Everything of tether's in it is released by the same plan: its pins are unpinned, and its branches deleted under the `--prune-bookmarks` rules. Without `--prune-bookmarks` a branch blocks the store; with unpinned writes it needs `--force-prune`.
5.  The backend confirms nothing else remains: the base branch at its initial state, the marker, the refs the plan deletes, and no uncommitted files in a store with a working area (a git checkout). "Cannot tell" means keep.

Otherwise the plan carries a `keep-store` naming what is in the way. `delete-store` runs last, behind a `store_empty` precondition asked again with nothing ignored, and the backend checks owner and emptiness once more itself. A tag or branch that appeared in between stales the plan. `undo gc` reports a deleted store as irreversible.

``` bash
tether gc --prune-bookmarks --delete-stores               # shows delete-store / keep-store
tether gc --prune-bookmarks --delete-stores --no-dry-run  # ...and applies it
tether gc --prune-bookmarks --delete-stores --force-prune # ...overriding rules 3 and 4
```

Fetch every bookmark before you run it: a bookmark another clone pushed is a reference `gc` can only count once it is in your history.

Two more indexes and one escape hatch round this out:

- **Stores this clone touched.** Every store a checkout forks a branch in or pins is recorded in `tether-touched.jsonl`. `gc --delete-stores` releases this dataset's dead refs there even when no manifest names the store any more, as an abandoned bookmark leaves it. So when another actor abandons refs in *your* created store, *their* next `gc --delete-stores` cleans them out (rule 3 binds them too), and yours then finishes. An entry goes (`forget-touched`) once nothing of the dataset's is left.
- **A store whose creator is gone.** Any clone can reclaim a created store by naming it: `gc --delete-stores --store KIND=LOCATOR` (repeatable; `LOCATOR` is the [uri](../reference/handles.FileHandle.md#tether.handles.FileHandle.uri)). The same five rules apply, with the marker as the authority, and a store with no marker is refused.

``` bash
tether gc --prune-bookmarks --store icechunk=s3://bucket/probes/emb.icechunk --no-dry-run
```

Two consequences. A clone elsewhere has its own (empty) index, so it reclaims the stores *you* created only when named by locator. And a store created on the trunk and written through its base branch is kept forever by rule 5: create on a bookmark when you mean to throw the store away.


# Recap

- Plans: `--dry-run` shows store writes, `--plan FILE` saves them, and `--from-plan FILE` applies them and refuses if anything moved.
- `--pin record` skips the native ref and forks from the recorded state: cheaper, and bound by retention.
- Drop commits with `tether abandon`, `tether drop`, or the VCS's own tools; what the VCS cannot see, tether does not protect.
- `gc` releases orphaned pins this clone created, and never deletes a branch on its own. `--prune-bookmarks` deletes gone bookmarks' branches when nothing on them would be lost, and `--force-prune` regardless.
- `gc --delete-stores` deletes a store `add --create` made once nothing references it; the owner marker decides, never a manifest.
