Disk fill guard design for the existing exhaustion-trajectory desktop watcher stack.

Recommendation: keep disk watching as a standalone `disk-fill-notify` companion service.
Reason: the existing memory watcher is deliberately small and memory-specific. Disk adds mount discovery, inode accounting, hot-path sizing, and cleanup logic with a different failure surface. A companion service preserves isolation while reusing the same notification contract and log file.

Signals

- Watch the real mounts behind `/`, `/home`, `/tmp`, `/boot`, and `/boot/efi` by parsing `/proc/self/mountinfo` and de-duplicating repeated targets.
- For each watched mount collect:
  - byte headroom: `f_bavail * f_frsize`
  - byte total: `f_blocks * f_frsize`
  - inode headroom: `f_favail` when available, else `f_ffree`
  - inode total: `f_files` when non-zero
  - filesystem type and `is_tmpfs`
- Skip inode ETA on filesystems that report zero/unknown inode totals.

Trigger model

- Use the same runway model as memory: keep a recent history of free bytes and free inodes per mount, compute recent burn rate, and alert only when the projected ETA to a floor is short.
- Byte exhaustion floor: configurable percent of total bytes free.
- Inode exhaustion floor: configurable percent of total inodes free.
- Stable-but-full mounts do not alert because the burn rate is zero or negative.
- A mount already below the floor only alerts if it is still shrinking.
- `/tmp` on `tmpfs` is treated as a normal byte runway plus an annotation in the alert body because filling it also consumes RAM.

Culprit identification

- Do not recursively size `/` or an entire home tree on every poll.
- Only sample suspects when a mount is hot:
  - projected ETA below a broader scan threshold, or
  - free space/inodes already low, or
  - the mount is `/tmp`
- Candidate selection is bounded:
  - `/tmp`: immediate children under `/tmp`
  - `/boot`: immediate children under `/boot`
  - `/`: fixed suspects such as `/var/tmp`, `/var/log`, `/var/cache`, and `/tmp` when it is not a separate mount
  - home mount: `~/.cache`, `~/.local/state`, `~/.local/share`, `~/.npm`, `~/.cargo`, `~/.cache/uv`, `~/.cache/pip`, trash, plus recent top-level children and common nested growth sinks like `node_modules`, `log`, `logs`, `.cache`, `dist`, and `build`
- Measure candidates recursively with stdlib only, never follow symlinks, and never cross to another mount.
- Keep a short history per suspect path and rank by recent growth when available; otherwise fall back to current size or current entry count. This preserves the “trajectory first” philosophy without paying continuous `du` cost.

Notification behavior

- Reuse `notify-send --wait -u <urgency> -A ...` and append events to `~/.local/state/mem-guard/events.log`.
- Keep cooldowns per culprit path plus metric kind.
- Alert body names the hot mount, reason, whether it is `tmpfs`, and the top suspects.

One-click remediation

- Only offer destructive action for `/tmp`.
- Action text: `Clean stale /tmp junk`.
- Cleanup scope:
  - only top-level entries directly under `/tmp`
  - only entries owned by the current user
  - only entries older than a minimum age
  - never follow symlinks
  - never cross mount boundaries
  - never touch protected names or patterns
  - skip sockets, FIFOs, device nodes, and anything with an open descendant visible via `/proc/*/fd`
- Removal is recursive for eligible directories, but only inside that single top-level entry. It does not blindly `rm -rf /tmp`.
- If nothing is safe to remove, log and notify that no cleanup candidate was eligible.

Failure handling

- Permission failures, disappearing files, and proc races are non-fatal and only affect the specific path.
- Culprit sizing is intentionally approximate under load. Correctness and safety of cleanup take priority over perfect attribution.
- Open-file detection uses `/proc/*/fd`; it reduces risk materially but cannot prove that every kernel reference is absent. The age gate, ownership gate, type gate, and top-level-entry-only rule are the primary safety controls.
