Undo and the op journal
Every mutating command in hav is exactly one op: a journal row carrying the verb, its arguments, and a one-line human summary. The load-bearing part is a complete before and after view of the repo's mutable state: every buoy position, every knot's current version, every workspace row.
Restoring a view restores the repo. That single idea gives hav transactional safety, perfect undo, and an audit trail, all from one table.
Undo and redo
hav undo # restore the view before the op at the cursor; cursor moves back
hav redo # re-apply the view after the next op; cursor moves forward
hav op log # the numbered journal, cursor marked @- Files come back too. Restoring a view re-projects the current workspace's tree to its restored snap. Undo covers the files on disk as well as the metadata.
- Undo is always redoable.
undoitself captures first, so the pre-undo tree is recorded before anything is restored. - Output names what changed, always:
undid op-413 (tie: main -> kn-behz) · main restored to kn-qwmr · redo available. Never a silent toggle. - New mutations while the cursor is rewound simply make the redo tail unreachable (rows are kept).
- A cascade triggered by a command records inside that command's op, so one undo restores the rewrite and every descendant it re-merged.
Reads (log, diff, show, conflicts, ws list, a no-change st) record
nothing. Two maintenance verbs (pack, sweep) and retention policy
(op limit) change nothing any view references, so they record no op either.
Push mutates the remote (atomically) and records nothing locally.
Atomicity
Each mutating verb is one SQLite transaction with the write lock taken up front and the before-view captured inside it. A failed multi-step mutation, such as a tie that loses a race or a sync that dies mid-merge, rolls back completely. There is no partial state, no lock file, and no repair command, because there is nothing to repair.
Retention: the journal window
The journal keeps the last 50 ops by default. hav op limit [<n>] shows
or sets the window (0 = unlimited), and op ids never renumber. Undo reaches
exactly as far back as the window, and stepping past it errors, naming the
discarded op.
Lowering the limit below what's currently retained discards history, so it
asks for an explicit y. Once set, the limit is standing consent, and the
oldest rows fall off silently as new ops append.
Nothing reachable from a retained op is ever deleted. That is the system's data-safety invariant, scoped by retention on purpose so that mistakes stay escapable, but only explicitly.
Sweep: the physical complement
Aged-out journal rows leave orphans: draft knot versions nothing references, objects no surviving snap can reach. They cost disk rather than correctness, and hav never reclaims disk behind your back.
hav sweep is the explicit reclaim: it drops knot versions that are neither
current nor named by any retained view, deletes objects unreachable from any
surviving snap or pinned edge, dissolves packs whose members
dropped, and compacts the database.
The canonical rescue: you accidentally captured node_modules/. Add it to
.havignore, keep working until the capture ages out of the 50-op window (or
lower the window), then run hav sweep. The database actually shrinks.
Sweep runs on workspace repos only (a hub's rows are governed by push monotonicity), never touches sealed rows, and refuses to run while any sealed crew is locked, so run it on a machine holding your keys.
The journal as a bug report
The journal is also why hav bugs are unusually reproducible: hav op log -n 20 is a numbered account of every mutation leading up to the problem, and the
views inside .hav/repo.db contain the full before/after of each. See
reporting bugs.