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 keeps gc inside this dataset’s namespace.
  • Fork: a writable branch created off a pin, or for pin = "record" objects off the recorded state. tether new -b NAME decides 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 and gc apply.
  • 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 undo takes 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, pull is fetch, and promote is the push to main.

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, undo and repair cover only what tether did to the stores.