ADR-0025 — Watcher-as-hint + authoritative rescan + content-hash truth
- Status: Accepted
- Date: 2026-06-11
- Related: sync/04 file watching, ADR-0022, ADR-0016
Context
To sync promptly, the client watches the filesystem. But every OS watcher is lossy:
Linux inotify drops events on queue overflow (default 16,384) emitting IN_Q_OVERFLOW;
macOS FSEvents coalesces and can demand MUSTSCANSUBDIRS; Windows ReadDirectoryChangesW
returns 0 bytes (all events lost) on buffer overflow; none work on NFS/SMB; inotify cannot
even tell you which process caused an event. A correctness model that trusts the watcher
will silently miss changes — the canonical “it didn’t sync my file” bug.
Decision
Treat the watcher as a latency optimization only; make authoritative scanning + content hashing the source of truth:
- Watcher → debounce/settle → ignore-filter → stat → hash-on-change updates the Local tree quickly in the common case.
- Scanning is authoritative: full scan on startup; forced rescan of the affected subtree on any overflow/coalesce-loss signal; an infrequent scheduled deep scan re-hashes regardless of mtime to catch forged timestamps, bitrot, and watcher gaps.
- Content hash (BLAKE3, ADR-0016) is truth;
mtimeis only a fast-path hint. - Self-write suppression: record
(path, expected hash)before applying downloads so our own writes don’t loop back as user changes (necessary since inotify can’t attribute events). - Polling fallback for network filesystems with no native events.
Consequences
Positive
- Correctness is independent of watcher reliability → no silent missed changes; a buggy or overflowing watcher only adds latency, never corruption.
- Hash-as-truth defeats mtime forgery/skew and detects real vs spurious changes.
- Self-write suppression eliminates the upload feedback loop.
Negative / costs
- Periodic/triggered scans cost IO/CPU at scale (mitigated by the
(inode,mtime,size)fast-path, parallel hashing, ignore-rules, scoped rescans). - Watch-descriptor memory for huge trees (bounded; fall back to scan-heavier mode).
Alternatives considered
- Trust the watcher (no rescan): simplest, but guarantees missed events under load / on network mounts. Rejected.
- Pure polling (no watcher): correct but high-latency/CPU. Fallback only.
- fanotify: more capable, needs privileges, not portable. Not required.
Scaling
Stat fast-path makes rescans stat-bound; ignore-rules (.git, node_modules, build
dirs) prevent event storms from blowing the inotify queue; deep scans run off-peak.