Concepts
tether borrows jj’s working-copy and operation-log ideas (new REV to start on top of a commit, the stale-working-copy check, an op log with undo). It applies them to external systems that keep their own history. This page is the model in one sitting; the Overview has the eight terms it uses.
The working copy is the manifests
A dataset is a directory holding tether.toml and one manifest per object, .tether/objects/<key>.toml. A manifest names the object (kind and locator), its policy, and once committed its recorded state and pin. Committing a dataset commits those manifests to the enclosing git or jj repository, so history, branching, workspaces and sharing are the VCS’s. What the VCS cannot see, what tether did to the stores, goes in tether’s operation log.
Commits hold pins; bookmarks hold branches
Two things are versioned, in two places.
A dataset commit records, for every object, an exact state and, where the system can hold one, a pin: a native ref that keeps that state alive. Nothing in a commit points at anything that moves: not a branch, not “latest”, not a path’s current contents. That is what makes tether open KEY --rev C a reproducible read months later, and what verify checks.
A bookmark is where writes go. The working copy (.tether/workspace.toml, per checkout) records which bookmark it works on and, per object, the branch that stands for it: its working ref.
| Working copy is on | Each Forkable object’s working ref | Fingerprinted by commit? |
|---|---|---|
the trunk bookmark (main) |
its upstream branch (locator.branch) |
yes: the head is what the next commit pins |
| another bookmark, branch created | tether.ws.<dataset>.<bookmark> in its system |
yes |
| another bookmark, branch not yet created (lazy) | pending: forked from the pin on the first writable open | no: until then it sits at the pin |
| no bookmark | none: read-only | no |
Objects whose system has no branches (files, Delta, DuckLake) have no working ref: they keep their pin until a trunk pull reads them. The rule is that commit contacts an object only if this checkout can have moved it, the way a file you did not edit stays as the parent commit had it. locator.branch is the object’s upstream: what the trunk bookmark stands for, what pull reads, and what promote lands on.
State, pin, fork
For each object a backend implements three things, in increasing order of what the system must support:
- State: what the object is now, such as an Icechunk snapshot_id, a Neon
(lsn, commit_xid, branch), a Delta version, or a file’s sha256. Comparing states is how drift is detected without watching anything. - Pin: a durable native reference to a state (an Icechunk tag, a Neon child branch, a git, Lance or Dolt tag), named
tether.<dataset-id>.<hash>. The hash is content-addressed from(kind, locator identity, state), so re-committing an unchanged object is a no-op. The dataset id keepsgcinside this dataset’s namespace. - Fork: a writable branch created off a pin, or for
pin = "record"objects off the recorded state.tether new -b NAMEdecides one per Forkable object,tether.ws.<dataset-id>.<bookmark>, created by the first writable open (or at once with--eager).
Capability tiers
Backends declare capabilities, and commands degrade explicitly by tier:
- Observed (FINGERPRINT): drift detection only; commit records the state as
recoverable = false. A local directory, an unversioned prefix. - Addressable (
+ ADDRESSABLE): the recorded state can be read back later with nothing to create or collect. An object in a versioned bucket, a Delta version, a DuckLake snapshot. - Pinnable (
+ PIN): a durable native ref can be created and deleted; verify andgcapply. - Forkable (
+ FORK): a writable branch can be made off a pin, so new forks and open returns a writable handle. Icechunk, Neon, Iceberg, Lance, Dolt, git.
Orthogonal flags such as PROMOTE, MERGE, CONDITIONAL_REF and DIFF are listed per backend in Backends, and defined in Writing a Backend.
Handles, not data
tether is never in the data path. open returns a typed handle carrying the native address (an Icechunk session on the right branch or tag, a Postgres URL, a DeltaTable at a version) and steps aside. You read and write with the system’s own library.
Store writes are plans
Most commands that write to a store compute a read-only plan, then apply it; apply refuses (StalePlanError) if the world no longer matches what the plan saw (Plans).
jj or tether?
Both work on the same repository, so the question comes up constantly. The rule: an operation gets a tether command when it has two halves, one in the VCS and one in the stores, that must happen together. If it only touches files and history, use jj or git directly. tether notices what it needs to: a moved working copy makes objects stale, and a vanished dataset commit shows in status and ops.
The nouns map one to one. A jj bookmark stands for one branch per system. jj’s snapshot on every command is tether’s snapshot on demand: a concurrent fingerprint on verify, status --snapshot and snapshot, and of the bookmark’s branches on commit. A stale working copy is a bookmark whose committed state changed after its branches were forked. A detached head is an object registered --at a state, until a trunk pull moves it onto the branch. A revision with no bookmark is a read-only working copy.
| You want to | Use | Because |
|---|---|---|
| record the dataset’s state | tether commit (jj: jj commit) |
pins the bookmark’s branches, writes their states into the manifests, commits, and moves the bookmark |
| start a line of work | tether new -b NAME [REV] (jj: jj new -b) |
creates the bookmark and a branch per system named after it |
| switch to a bookmark | tether new NAME (jj: jj new <bookmark>) |
moves the working copy and adopts that bookmark’s branches |
| look at another revision without writing | jj new REV / git switch |
nothing to fork; status says the working copy left the bookmark |
| compare two positions | tether diff [A] [B] (jj: jj diff) |
an object-level manifest diff; --content adds native diffs |
| browse an object’s own history | tether log KEY (jj: jj log) |
the system’s snapshots, versions or commits; the ids feed add --at |
| take what upstream has now | tether pull on main (jj: jj git fetch) |
reads every upstream branch and branch-less object, and commits what moved |
land a bookmark on main |
tether promote (jj: jj bookmark set main -r feature) |
moves each system’s upstream branch, then the trunk bookmark |
| throw a bookmark away | tether drop NAME |
drops its commits and the bookmark, then releases its pins and branches |
| reset one object to an older state | tether restore KEY --from REV (jj: jj restore --from) |
the branch lives in a store; everything else stays |
| drop dataset commits | tether abandon REV (jj: jj abandon) |
descendants keep their manifests, and the freed pins are planned for gc |
| describe, squash, rebase, split, merge | jj / git | manifests are files; run tether gc if commits disappeared, and tether verify and tether new after a merge |
| rename or delete a bookmark | jj bookmark rename/delete |
status then says so; gc --prune-bookmarks releases the old branches |
| push, fetch, review | jj / git | Sharing and CI |
| open or finish a second checkout | jj workspace add; tether forget-workspace |
each checkout is its own tether workspace; forgetting one leaves the bookmark’s branches |
| release what history no longer names | tether verify, then tether gc |
pins and branches live in the stores |
| undo tether’s last operation | tether undo (jj: jj undo) |
the VCS cannot see what tether did to the stores (Undo) |
| rebuild a pin or branch a manifest names | tether repair |
manifests are the source of truth |
| publish history as SQL | tether export / publish / import (experimental) |
a derived view of the manifests |
Two habits keep the halves aligned: tether status after any VCS operation that moved the working copy or changed manifests, and tether new before writing again.
Where tether is not jj
- Writes need a bookmark. A store branch must exist before a write can land, so the bookmark is the handle both sides share. Reads (
open --rev, status, verify) work on anonymous changes, as in jj. - commit moves the bookmark. jj bookmarks do not follow commits; a tether bookmark must, or its branches’ heads would stop matching the commit that names them. It happens in one jj operation, so
jj undotakes both back. commit also starts nothing new. - Snapshots are on demand. Reading a state is a network round trip, so status is local, and
status --snapshot, verify and commit fan out. - The stores are remotes. Pins are what commits reference, bookmark branches are local branches,
pullis fetch, and promote is the push tomain.
What tether does not do
- Sit in the data path, or run between commands: there is no daemon, and every state is compared when a command asks.
- Cross-system atomicity: a commit records each state within a capture window, and point-consistency requires quiesced writers.
- Reimplement history, branching or sharing, or query across systems.
ops,undoandrepaircover only what tether did to the stores.