mcp-watcher: safety-poll fallback for dropped inotify events #3

Open
angus wants to merge 1 commit from foreman/safety-poll into main
Contributor

Summary

Adds a periodic safety-poll timer (default 30s) to InboxWatcher that calls drain() unconditionally, covering the case where chokidar/inotify silently drops an IN_MODIFY event.

Observed twice in production: a ping is appended to foreman.inbox, file mtime updates, but no event surfaces to the watcher. A sibling-file touch unblocks it. Root cause is Linux inotify reliability under brief idle gaps + atomic writes — chokidar relies on it without a polling fallback.

drain() is already idempotent (HWM comparison short-circuits when nothing's new), so steady-state overhead is one stat + JSON parse per poll cycle. The event-driven path remains primary; the poll just masks the rare miss within the cycle interval.

Changes

  • New safetyPollMs option on WatcherOptions. Default 30_000; set to 0 to disable.
  • start() arms a setInterval; stop() clears it before closing chokidar.
  • Two new tests in watcher.test.ts:
    • safety-poll delivers when fs-event never fires (simulates dropped inotify)
    • safetyPollMs: 0 truly disables the timer

Test plan

  • npm run typecheck clean
  • npm test — 39/39 pass (was 35; added 2 tests for new behavior, +2 pre-existing)
  • Restart watcher on laptop after merge, observe HWM advances within 30s of any inbox append even when chokidar misses an event

🤖 Generated with Claude Code

## Summary Adds a periodic safety-poll timer (default 30s) to `InboxWatcher` that calls `drain()` unconditionally, covering the case where chokidar/inotify silently drops an `IN_MODIFY` event. Observed twice in production: a ping is appended to `foreman.inbox`, file mtime updates, but no event surfaces to the watcher. A sibling-file touch unblocks it. Root cause is Linux inotify reliability under brief idle gaps + atomic writes — chokidar relies on it without a polling fallback. `drain()` is already idempotent (HWM comparison short-circuits when nothing's new), so steady-state overhead is one stat + JSON parse per poll cycle. The event-driven path remains primary; the poll just masks the rare miss within the cycle interval. ## Changes - New `safetyPollMs` option on `WatcherOptions`. Default `30_000`; set to `0` to disable. - `start()` arms a `setInterval`; `stop()` clears it before closing chokidar. - Two new tests in `watcher.test.ts`: - safety-poll delivers when fs-event never fires (simulates dropped inotify) - `safetyPollMs: 0` truly disables the timer ## Test plan - [x] `npm run typecheck` clean - [x] `npm test` — 39/39 pass (was 35; added 2 tests for new behavior, +2 pre-existing) - [ ] Restart watcher on laptop after merge, observe HWM advances within 30s of any inbox append even when chokidar misses an event 🤖 Generated with [Claude Code](https://claude.com/claude-code)
angus added 1 commit 2026-05-23 14:58:33 -03:00
Adds a periodic timer (default 30s) that calls drain() unconditionally,
covering the case where chokidar/inotify silently drops an IN_MODIFY
event. Observed twice in production: ping appended to inbox, file mtime
updated, but no event delivered to the watcher; a sibling-file touch
unblocked it. Root cause is Linux inotify under brief idle gaps + atomic
writes — not consistently reliable on its own.

drain() is already idempotent (HWM comparison short-circuits when
nothing's new), so the steady-state overhead is one stat + JSON parse
per poll cycle. Event-driven path remains the primary; the poll just
masks the rare miss within the cycle interval.

- safetyPollMs option: default 30_000, set to 0 to disable
- stop() clears the interval before closing chokidar
- Two new tests: safety-poll delivers when fs-event never fires;
  safetyPollMs:0 truly disables the timer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin foreman/safety-poll:foreman/safety-poll
git checkout foreman/safety-poll

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout main
git merge --no-ff foreman/safety-poll
git checkout foreman/safety-poll
git rebase main
git checkout main
git merge --ff-only foreman/safety-poll
git checkout foreman/safety-poll
git rebase main
git checkout main
git merge --no-ff foreman/safety-poll
git checkout main
git merge --squash foreman/safety-poll
git checkout main
git merge --ff-only foreman/safety-poll
git checkout main
git merge foreman/safety-poll
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: foreman/agent-watcher#3
No description provided.