Skip to content

fix(checkpoints): a revert never applies one checkout's snapshots to another - #912

Merged
badcuban merged 2 commits into
mainfrom
fix/revert-after-checkout-switch
Nov 14, 2026
Merged

badcuban merged 2 commits into
mainfrom
fix/revert-after-checkout-switch

Conversation

@badcuban

Copy link
Copy Markdown
Collaborator

Two fixes that follow #911 (a thread working in a checkout another thread already has).

A revert could apply one checkout's snapshots to another

A thread can move to another checkout between turns: the user's own switch, an agent's move, a session that settled somewhere else. Every checkout of a repository shares its checkpoint refs, so a revert to a turn from before the move applied snapshots of the folder the thread had left to the folder it was in. For a thread alone in its worktree it restored the other folder's whole tree over it.

  • Every snapshot now records the checkout it was taken in, as a threadlines-checkout trailer beside threadlines-head: . for the main checkout, else worktrees/<name> (git's own name for it, so a renamed or symlinked folder is still the same checkout).
  • checkpointCheckouts on the VCS driver and CheckpointStore answers, per ref: here, elsewhere or unknown (a snapshot from before this, or a ref that is gone).
  • CheckpointRevert.resolveContext refuses a revert when a turn it would undo has a completion or starting snapshot from another checkout. When only the target is from another checkout (a revert to the turn the thread moved after), it restores from the first undone turn's own starting snapshot, which is this folder right before that turn.
  • The thread.checkout.moved activity from feat(threads): an agent can put a thread in a checkout another thread already has #911 now only stands in for snapshots that don't say where they were taken.

Capture reads the checkout id alongside the head commit, so it adds no wait to a turn.

Starting in another thread's checkout failed behind a symlink

A live run of #911 with a real agent failed with "is no longer a worktree of this project" because the data folder sat behind a symlink (macOS /var for /private/var): git lists a worktree by its real path and the thread had recorded it through the symlink. The setup now compares the two as the disk resolves them.

Checked

  • vp fmt, vp lint, vp run typecheck, vp run test (full suite)
  • New tests: real git with two checkouts and a renamed worktree (CheckpointStore.test.ts), the revert rules (CheckpointRevert.test.ts), the symlink case (ThreadBootstrap.test.ts)
  • A live run of feat(threads): an agent can put a thread in a checkout another thread already has #911's chain with Codex on a throwaway server: a reviewer started in a coder's checkout, and an architect moved into it and kept its conversation

Not checked

  • A revert across a checkout move was not run in the live app; it is covered by the tests above.
  • Known and left alone: the worktree removal guard and the cleanup list compare path spellings without resolving symlinks, so a thread recorded under a symlinked path does not block removing the real one.

…older is behind a symlink

Starting a thread in another thread's checkout failed with "is no longer a
worktree of this project" whenever the data folder sat behind a symlink
(macOS /var for /private/var): git lists a worktree by its real path, and
the thread had recorded it through the symlink. The setup now compares the
two as the disk resolves them. Found in a live run with a real agent.
…another

A thread can move to another checkout between turns (the user's own switch,
an agent's move, a session that settled somewhere else), and every checkout
of a repository shares its checkpoint refs. A revert to a turn from before
the move then applied snapshots of the folder the thread had left to the
folder it was in, and for a thread alone in its worktree restored the other
folder's whole tree over it.

- Every snapshot now records the checkout it was taken in, as a
  `threadlines-checkout` trailer: `.` for the main checkout, else
  `worktrees/<name>`, so a renamed or symlinked folder is still the same
  checkout.
- `checkpointCheckouts` answers, per ref, here, elsewhere or unknown.
- A revert is refused when a turn it would undo has a completion or
  starting snapshot from another checkout. When only its target is from
  another checkout, it restores from the first undone turn's own starting
  snapshot, which is this folder right before that turn.
- The move an agent made, on the thread's record, now only stands in for
  snapshots that don't say where they were taken.
@vercel

vercel Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadlines Ready Ready Preview Nov 14, 2026 10:20am UTC

Request Review

@badcuban
badcuban added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit ccd159e Nov 14, 2026
33 of 35 checks passed
@badcuban
badcuban deleted the fix/revert-after-checkout-switch branch November 14, 2026 10:37

This branch was successfully deployed

1 active deployment
Preview — d7786870 Deployed Nov 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant