agent-watcher/mcp-watcher
bob-boat 95ff60ce94 mcp-watcher: safety-poll fallback for dropped inotify events
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>
2026-05-23 12:54:26 -04:00
..
src mcp-watcher: safety-poll fallback for dropped inotify events 2026-05-23 12:54:26 -04:00
test mcp-watcher: safety-poll fallback for dropped inotify events 2026-05-23 12:54:26 -04:00
.gitignore Layer 2 MCP Watcher v0 scaffold 2026-05-06 17:44:57 -03:00
install.sh Fix env var: CLAUDE_CONFIG_DIR not CLAUDE_HOME 2026-05-06 18:05:22 -03:00
package-lock.json Layer 2 MCP Watcher v0 scaffold 2026-05-06 17:44:57 -03:00
package.json Layer 2 MCP Watcher v0 scaffold 2026-05-06 17:44:57 -03:00
README.md Fix env var: CLAUDE_CONFIG_DIR not CLAUDE_HOME 2026-05-06 18:05:22 -03:00
tsconfig.json Layer 2 MCP Watcher v0 scaffold 2026-05-06 17:44:57 -03:00
vitest.config.ts Layer 2 MCP Watcher v0 scaffold 2026-05-06 17:44:57 -03:00

agent-watcher-mcp (Layer 2)

The MCP Watcher: a Claude Code stdio MCP server that surfaces ping-inbox events into your Claude Code session via Channels, and exposes reply tools (ack / respond / mark_handled) so Claude can react.

Spec: ../spec/agent-watcher.md §4.

What it does

  1. Resolves identity (env / $CLAUDE_HOME/ping-agent / ~/.ping-agent).
  2. Writes pings/.<agent>.watcher-active so the existing agent-ping UserPromptSubmit hook stands down on this host.
  3. Watches pings/<agent>.inbox via inotify (chokidar).
  4. On change, drains unread pings (since HWM), applies sentinel deferral (warn-after-3), and emits each as a notifications/claude/channel event.
  5. Exposes three MCP tools so Claude can ack, respond, or mark a ping handled.
  6. Removes the sentinel on graceful shutdown.

Install

Per CLAUDE.md rule #2, the agent does not install on itself. A human runs:

cd /path/to/agent-watcher/mcp-watcher
./install.sh

This installs deps, builds, symlinks the binary into ~/.local/bin/, adds .stignore patterns, and prints the mcp.json snippet to paste into Claude Code's config.

Launch

Channels is in research preview. Start Claude Code with:

claude --dangerously-load-development-channels server:agent-watcher

Sandbox testing

To run a separate Claude Code session with a different identity (so it won't compete with the default ~/.ping-agent for inbox reads):

mkdir -p ~/.claude-sandbox
echo sandbox > ~/.claude-sandbox/ping-agent
CLAUDE_CONFIG_DIR=~/.claude-sandbox \
  claude --dangerously-load-development-channels server:agent-watcher

The sandbox session reads pings/sandbox.inbox and writes pings/.sandbox.watcher-active — fully isolated from prod.

How it interacts with agent-ping

The agent-ping UserPromptSubmit hook checks for the sentinel file pings/.<agent>.watcher-active at startup. If the file is present and the PID inside is alive, the hook stands down — the watcher is the delivery primitive. If the watcher exits (graceful or crash with stale sentinel cleared by the hook's age check), the hook resumes async-on-next-prompt delivery.

This means you can run with the watcher OR the hook OR both configured — the sentinel arbitrates.

Tests

npm test            # unit tests via vitest (35 currently passing)
npm run typecheck   # tsc --noEmit
npm run build       # tsc → dist/

Observability

  • Logs to stderr (visible via claude --debug or the Claude Code MCP debug log at ~/.claude/debug/<session-id>.txt).
  • Sentinel file content includes PID + start time for the hook's age/liveness check.

Limitations / v2

  • Sentinel hook coexistence requires the agent-ping hook to know how to read the sentinel. PR pending against agent-ping to add the check.
  • No reconnection / restart on Claude Code session restart — Claude Code spawns the subprocess anew each session, so the watcher re-drains from HWM cleanly.
  • One watcher process per agent identity. Two sessions with the same identity contending on the same inbox is undefined behaviour (use CLAUDE_HOME to scope identities).
  • Channels is research preview; if the API changes, expect to update the meta-key sanitization or notification shape.