Skip to content

fix(watch): an arriving directory re-decides the worktrees beneath it - #703

Merged
atlas-from-plumb merged 10 commits into
mainfrom
atlas/plan-558
Oct 16, 2026
Merged

atlas-from-plumb merged 10 commits into
mainfrom
atlas/plan-558

Conversation

@atlas-from-plumb

@atlas-from-plumb atlas-from-plumb commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

PLAN-558, the residual of #700's review. The watcher's worktree skip (internal/fswatch/worktrees.go) cached a "worktree" verdict at X/wt, and that verdict outlived X. So did a stale git record for X/wt. Remove X, then move an ordinary tree into its place (mv), and the arrival's expansion treated the ordinary wt as a worktree, silencing every edit inside it.

  • Arrival keyed on directory identity (arrivalLocked). An event at P is an arrival only if a directory now stands at P. A worktree verdict or record at or beneath P is dropped (or superseded) only where its directory still exists and is a different one (dev+inode, taken when the belief was formed).
    • A directory being removed keeps its belief, and so does one moved away and back. The first version keyed on the event op, and review round 1 measured a BLOCKING leak on Linux: inotify reports a move AWAY as Remove|Rename.
    • Identity is dirID: device, inode and birth time (statx STATX_BTIME on Linux, Birthtimespec on darwin; os.SameFile alone elsewhere). Linux CI caught inode reuse: on ext4 a tree made right after rm -rf reuses the freed inodes, which the container's overlayfs hid. The birth time tells a reused inode apart.
  • Verdicts are kept in separate worktree and ordinary sets, so the arrival scan covers only the few worktrees. A directory that becomes a worktree loses its stale ordinary verdict. A full worktree set drops only the directories that no longer exist.
  • Unchanged from fix(watch): the shared file watcher leaves linked git worktrees out #700:
    • a directory's own Remove re-decides nothing;
    • positive verdicts stay sticky through a removal;
    • loss is lazy.

Verification

  • Unit tests (fake directory identities):
    • a parent's arrival re-decides beneath it (cached-verdict and stale-record shapes, plus a real-worktree control);
    • a move is not an arrival (moved away; moved away and back, warm and cold);
    • a moved-back verdict and a moved-back record each keep their identity;
    • only its own subtree; a stale ordinary verdict; the cap.
  • Real backend: an ordinary tree mv'd over a worktree's parent is watched. Rename away and back, then rm -rf, stays quiet: in the gate's Linux image it fails 7/10 on the previous head and passes 10/10 here. The whole fswatch package passes 10/10 under -race in that image.
  • Mutation: 17 mutants. All KILLED except two equivalent ones (the exists check, which replacedLocked's gone-keeps rule makes redundant, and the re-remember guard, which is perf-only).

Closes PLAN-558 (ops board).

🤖 Generated with Claude Code

atlas-from-plumb and others added 10 commits October 16, 2026 14:49
…neath it

PLAN-558, the residual of #700's review: a worktree verdict cached at X/wt
survived X being removed and an ordinary tree arriving whole at X (a mv), and
so did a stale bookkeeping record for X/wt — the arrival's expansion then
treated the ordinary wt as a worktree and its edits were silenced. A Create or
Rename of a directory now drops the worktree verdicts beneath it and
supersedes the records beneath it; a real worktree inside the arriving tree
still verifies as one. Worktree and ordinary verdicts are kept in separate
sets so that scan stays over the few worktrees, and a full worktree set drops
only directories that no longer exist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… a stale ordinary verdict

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…es the worktrees beneath it

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nt directory stands

Review of #703 (BLOCKING): inotify reports a directory moving AWAY as
Remove|Rename, and one moved away and back is the same directory; treating
either as an arrival dropped the worktree verdicts beneath it and superseded
their records, so the rest of a removal leaked (thousands of events measured
on Linux; main leaked none). Now nothing happens unless a directory stands at
the path, and a belief at or beneath it is dropped only where its directory
still exists and is not the one the belief was formed for (dev+inode, taken
when the verdict or record was formed). A directory being removed keeps its
belief. Known residual: a directory recreated with a reused inode looks
unchanged. Also: arrived() no longer re-remembers a cached worktree.

The Linux-image control: the new real-backend rename-away-and-back test fails
7/10 on the previous head and passes 10/10 here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
#703's Linux CI failed TestWatcher_AnOrdinaryTreeMovedOverAWorktreesParentIsWatched:
on ext4 the tree made right after `rm -rf` reused the freed inodes, so the
moved-in ordinary wt read as the old worktree (device + inode alike) and its
edits were silenced. The container gate's overlayfs hands out fresh inodes,
which hid it. dirID now adds the birth time where the platform reports one
(statx STATX_BTIME on Linux, Birthtimespec on darwin); elsewhere it falls back
to os.SameFile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…the filesystem records; a gone worktree keeps its verdict through an arrival

Review of #703: the reused-inode test skipped exactly when dirID's birth time
went missing, so a broken statDir or darwin birthTime survived it. It now
skips only where the platform genuinely records no birth time
(platformRecordsBirth, checked independently).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@atlas-from-plumb
atlas-from-plumb merged commit 537c9ad into main Oct 16, 2026
9 checks passed
@atlas-from-plumb
atlas-from-plumb deleted the atlas/plan-558 branch October 16, 2026 05:24
atlas-from-plumb added a commit that referenced this pull request Oct 16, 2026
An arriving directory re-decides the worktrees beneath it (#703).

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants