Living Document Notice
Published 2026-10-27. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.
Filesystem Watching Without Battery Drain
Summary
Naive file watching implementations poll directory trees or register indiscriminate filesystem listeners that wake hardware cores continuously. On laptops running on battery power, uncalibrated event loops drain watt-hours by preventing processor packages from entering low-power sleep states. The Tender daemon replaces periodic polling with native kernel notification APIs configured with adaptive debounce state machines.
By combining operating system kernel subscriptions with event aggregation windows, the daemon coalesces high-frequency text editor writes and Git checkout floods into single disk synchronization passes. This architecture minimizes wakeups and maintains sub-second sync latency without degrading battery life.
Kernel Notification Subsystems
Traversing directories via recursive directory walks consumes CPU cycles and disk I/O bandwidth. In a 50,000-note vault, a single traversal inspects directory dentries across multiple megabytes of filesystem metadata. Doing this every five seconds keeps hardware cores active and exhausts laptop battery reserves.
The Tender daemon uses native kernel notification subsystems:
- Linux: inotify watches aggregated through an epoll event loop.
- macOS / BSD: kqueue tracking file descriptor changes via EVFILT_VNODE.
- Windows: ReadDirectoryChangesW backed by asynchronous completion ports (IOCP).
These kernel subsystems notify the daemon only when directory entries change. When the vault remains idle, the Tender daemon process sleeps inside blocking kernel syscalls, consuming zero CPU cycles and permitting processor package sleep states (such as Intel C10 or AMD C6).
+--------------------------------------------------------------------+
| Debounce State Machine |
| |
| [ Inotify Event ] ---> ( Event Queue ) |
| | |
| v |
| [ Timer Active? ] |
| / \ |
| No/ \ Yes |
| v v |
| ( Arm 25ms Timer ) ( Reset or Extend Timer ) |
| | | |
| +-------+------+ |
| | |
| [ Ceiling Exceeded? ] |
| / \ |
| No/ \ Yes |
| v v |
| ( Sleep ) ( Force Flush Queue ) |
| | |
| v |
| [ Emit Journal Record ] |
+--------------------------------------------------------------------+
Atomic Writes and Editor Quirk Coalescing
Text editors rarely modify note files in place. Most modern editors perform atomic file saves using a temporary file pattern:
- Write buffer to a temporary file (
note.md.tmp.1234). - Flush temporary file to disk with
fsync. - Rename the temporary file over the original file (
rename("note.md.tmp.1234", "note.md")).
This sequence generates three distinct kernel events: IN_CREATE, IN_MODIFY, and IN_MOVED_TO. Processing each event independently creates redundant log entries and triggers unnecessary hash recalculations.
The Tender daemon applies a dynamic debounce state machine to coalesce burst events. When an event arrives for a specific file path, the daemon arms a 25-millisecond timer. If subsequent events for that same file path arrive before the timer expires, the timer resets. Once the debounce window elapses without new modifications, the daemon evaluates the file state once.
pub struct EventDebouncer {
pub pending_paths: HashMap<PathBuf, FileEventMask>,
pub window_ms: u64,
pub max_ceiling_ms: u64,
}
impl EventDebouncer {
pub fn record_event(&mut self, path: PathBuf, mask: FileEventMask, now: Instant) {
let entry = self.pending_paths.entry(path).or_insert(FileEventMask::empty());
entry.insert(mask);
}
}Managing Git Checkout Floods
Running git checkout or git pull inside a vault directory generates thousands of file modifications in milliseconds. If an indexer queues full hash operations for each file instantly, the system experiences severe thread pool contention and memory spikes.
The Tender daemon guards against event floods by imposing an upper ceiling on debounce windows. While individual file modifications settle within 25 milliseconds, sustained event floods activate batch consolidation:
| Event Mode | Trigger Condition | Debounce Window | Maximum Delay | Worker Queue Action |
|---|---|---|---|---|
| Interactive Typing | Single file modify | 25ms | 50ms | Incremental hash and journal append |
| Burst Edit | Multi-file rename | 50ms | 150ms | Coalesced path aggregation |
| Git Checkout Flood | > 100 events / 100ms | 100ms | 500ms | Pause hashing, scan git index, bulk sync |
When the kernel event rate exceeds 100 events per 100 milliseconds, the daemon switches from single-file tracking to a bulk directory reconciliation pass. This avoids redundant intermediate state computations while preserving file integrity.
The Linux inotify subscription explicitly isolates terminal writes:
int wd = inotify_add_watch(
inotify_fd,
vault_path,
IN_CLOSE_WRITE | IN_MOVED_TO | IN_DELETE | IN_EXCL_UNLINK
);Using IN_CLOSE_WRITE instead of IN_MODIFY prevents triggering on partial buffer flushes, ensuring that the daemon wakes only when file writes complete entirely.
- Directus Target: tender
- Garden Source Reference: Filesystem Notification Subsystems, Debounce State Machines, Kernel Event Loops, MOC - Ingestion & Capture, MOC - Local-First Systems and Synchronization, MOC - Bosun PKM Tools