SIGN IN SIGN UP

fix(watch): track watches by inode, so a recreated directory is not a corpse

Delete-and-recreate inside one tick killed the watcher and then lost the
recreated contents. `rm -rf src && mkdir src` — or two back-to-back branch
switches — stops the emitter on the delete, but by the time the tick runs the
path exists again, so the old `os.path.isdir(root)` test called it a genuine
death and exited 1. `sync_watches` could not rescue it either, because watches
were keyed by path string: a replacement was indistinguishable from an
untouched directory, so it was neither released nor re-adopted. The crash was
loud; the resulting gap was silent, because the restarted watcher's startup
reconciliation removes stale rows but never adds anything.

Probed on macOS: the emitter is dead 0.2s after the delete, the recreated
directory has a different inode, and files written into it produce no events
at all.

Watches now carry `(st_dev, st_ino, st_birthtime)` captured at schedule time:

- `sync_watches` compares the stored identity against the current stat, so a
  replaced directory is released and re-adopted in the same tick, and the
  existing dispatch of DirDeleted/DirCreated re-indexes both sides;
- liveness asks whether the dead thread's watch is still the live watch for a
  directory that is still the same directory, rather than whether something
  with that name exists now;
- the one case identity cannot see — an inode handed straight back on a
  platform without `st_birthtime` — is repaired by rescheduling the watch once
  per root and re-reading the directory. A second death of the same root is
  still reported, and the watcher still exits non-zero, so #811's crash stays
  loud.

Also from the same review:

- a directory adopted after startup is planned by `_plan_watch_subtree`, the
  same way startup plans the repository, so a module arriving from a branch
  switch no longer hands its own `node_modules` and `target` back to the OS;
- promotion to a recursive watch now releases every descendant watch, not just
  direct children, so none linger to duplicate events and burn budget slots.

Regression tests: a real `Observer` driven through delete-then-recreate,
asserting both that the watcher survives and that a file written into the
recreated directory reaches the graph. It fails on the previous implementation
with `RuntimeError('watch observer stopped: dead thread(s) Thread-4')`, and
with the exit suppressed it fails instead on "the recreated directory's files
never reached the graph" — the two halves of the same bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fHfGDiZedoxjpKzanHri3
T
Tirth Kanani committed
c4c4d703ae3ff9ec29338bd04ef47f6c2ef6c66b
Parent: db45999