A filesystem callback can arrive twice, arrive late, or never arrive. Treating each callback as an application-state transition makes a session monitor fast but fragile.
The safer design uses native events as wake-up signals. A relevant path schedules a bounded rescan. The parser reads current transcript evidence and decides the state. Periodic polling repairs any notification the operating system did not deliver.
Polling alone sets a latency floor
With a six-second polling interval, a correct your-turn state can appear six seconds late even when parsing takes milliseconds. Lowering the interval reduces the delay but increases idle scans across every known transcript root.
Native events remove that floor for ordinary writes. They tell the monitor that something changed under a watched root, so it can scan immediately.
The event does not need to describe the state change. It only needs to wake the classifier.
macOS watches roots, then filters paths
The macOS implementation uses one FSEvents stream over the existing Claude project, Codex session, and Claude Desktop metadata roots. It requests file-level events with a short latency.
The callback filters for transcript .jsonl files and the relevant Claude Desktop records. A matching path calls one change handler.
Parsing stays outside the callback. FSEvents may coalesce changes, so the handler asks, "What is true now?" rather than trying to replay an assumed sequence from notification flags.
Windows uses the same rule with different plumbing
Windows creates a FileSystemWatcher for each existing root and watches recursively. Changed, created, and renamed events can all indicate new session evidence.
One save may produce several callbacks. Atomic replacement may look like a rename. Each relevant callback therefore schedules the same rescan operation instead of applying a different state mutation.
The two platforms share the wake-up contract, not watcher syntax.
Keep polling for repair
Native notification channels are optimized for speed, not durability.
FSEvents can combine nearby changes. FileSystemWatcher has a finite buffer that can overflow during a burst. A root may appear after startup, and permissions can prevent a watcher from attaching.
Periodic polling covers those cases:
- discover roots that were missing at startup;
- rescan bounded transcript tails;
- restore the correct state after a missed or coalesced event.
The fallback should stay bounded. Recovery is not a reason to reread every transcript from byte zero.
Coalesce the work, not the truth
A burst of callbacks should not launch a burst of full refreshes. Coalesce nearby wake-ups, serialize the refresh path, and let another pass run when activity arrives during a scan.
Only the parser decides session state. Watchers and timers merely request another observation. This keeps retries idempotent and prevents callback count from becoming business logic.
Test recovery separately from latency
An event-triggered scan proves the fast path. It does not prove recovery.
Test both:
- append a completion event and verify the native watcher wakes the classifier;
- disable or bypass the watcher, then verify polling reaches the same state;
- create a transcript root after startup and verify a later sweep discovers it;
- send repeated callbacks and verify they do not duplicate a turn alert.
I help run Agent Island. The public v1.7.1 macOS build uses FSEvents, the Windows build uses FileSystemWatcher, and both retain periodic scanning.
Top comments (0)