Skip to content

fix(status): poll pidfile registry as fallback for missed fs.watch events#62

Open
raphapizzi wants to merge 1 commit into
Ron537:mainfrom
raphapizzi:fix/pidfile-registry-polling-fallback
Open

fix(status): poll pidfile registry as fallback for missed fs.watch events#62
raphapizzi wants to merge 1 commit into
Ron537:mainfrom
raphapizzi:fix/pidfile-registry-polling-fallback

Conversation

@raphapizzi
Copy link
Copy Markdown

Summary

Fixes #61 — a Claude Code session waiting on an approval prompt renders with the green pulsing "Running Tool" dot and never fires the attention bell.

ClaudePidfileRegistry was 100% event-driven on a single fs.watch() watcher with no polling fallback. On macOS, fs.watch (FSEvents) is well known to coalesce or drop notifications for in-place rewrites of small files — exactly how Claude Code mutates its pidfile from status: "busy" to status: "waiting", waitingFor: "approve <Tool>". When that event is dropped, the in-memory snapshot stays stale forever (until the next subscriber-driven re-scan or app restart), and the entire status / attention pipeline downstream of it (mapPidfileStatus, attentionService.ingestSessionUpdate, StatusDot) silently acts on the wrong state.

What changed

  • src/main/services/providers/claudePidfileRegistry.ts:

    • Add a 2-second polling rescan as a safety net alongside fs.watch. The watcher remains the low-latency primary path; polling guarantees eventual consistency.
    • scanAll now tracks whether anything actually changed and only calls notify() when changes are observed (or on the very first scan, to seed subscribers) — so steady-state polling does not spam downstream consumers.
    • refreshFile returns a boolean indicating whether the snapshot changed, so scanAll can aggregate without duplicating shallow-equal logic.
    • The poll setInterval is .unref()'d to keep CLI/test contexts from being held open.
  • tests/unit/claude-pidfile-registry.test.ts:

Behavior

  • Worst-case latency for a status transition (when fs.watch drops the event): ≤ 2s (was: never observed until next subscribe / app restart).
  • Best-case latency (when fs.watch fires): unchanged.
  • Steady-state CPU: one readdir + N small readFiles every 2s. Existing snapshots use shallowEqualSnapshot dedup; no work propagates downstream when nothing changed.

Test plan

  • node node_modules/vitest/dist/cli.js run tests/unit/claude-pidfile-registry.test.ts — 10/10 pass (8 existing + 2 new)
  • Full suite: node node_modules/vitest/dist/cli.js run — 330/330 pass
  • Typecheck: tsc --noEmit -p tsconfig.node.json — clean
  • Manual verification (will follow up with screen recording once dev build is running): trigger a Bash approval prompt and confirm the dot turns red + bell fires within ~2s without restarting the app.

🤖 Generated with Claude Code

…ents

fs.watch on macOS (FSEvents) is known to coalesce or drop notifications
for in-place file rewrites — the exact pattern Claude Code uses when
transitioning its pidfile from `status: "busy"` to
`status: "waiting", waitingFor: "approve <Tool>"`. When the registry
misses that transition, the entire session-status pipeline stays stuck:

- `mapPidfileStatus` keeps returning `executingTool` (green pulsing dot)
- `attentionService.ingestSessionUpdate` never sees `awaitingApproval`,
  so the attention bell never fires and no notification is created
- The user is left staring at a "Do you want to proceed?" prompt with no
  indication from the UI that their attention is required

Add a 2s polling rescan as a safety net. fs.watch is still the
low-latency primary path; polling closes the gap when an event is
dropped, guaranteeing eventual consistency.

Optimizations:
- `scanAll` now tracks whether anything changed and only calls `notify()`
  when changes are observed (or on the very first scan, to seed
  subscribers), keeping steady-state polling free of downstream
  parseSession work.
- `refreshFile` returns a boolean reflecting whether the snapshot
  changed, so `scanAll` can aggregate without duplicating shallow-equal
  logic.
- The poll timer is `.unref()`'d so it never holds the event loop open
  in CLI/test contexts.

Tests:
- New: polling fallback catches a `busy → waiting` transition when
  fs.watch is forcibly silenced (regression guard).
- New: polling fallback stays quiet in steady state (subscriber not
  spammed every 2s when nothing has changed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@raphapizzi raphapizzi requested a review from Ron537 as a code owner May 12, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Status dot stuck on green "Running Tool" while session is actually waiting for approval (no notification)

1 participant