$ watch transcript roots

Why Filesystem Events Still Need a Polling Fallback

Native filesystem events reduce notification latency, but they are not a durable transcript of every write. A session monitor should use them to trigger bounded rescans and let polling repair missed hints.

Verified against Agent Island v1.7.1 on 2026-07-30. The stable macOS build uses CoreServices FSEvents. The Windows build uses FileSystemWatcher. Both retain a periodic scan.

Polling creates a visible latency floor

A polling-only monitor learns about a new transcript line on its next tick. A six-second interval means the correct state can be six seconds late even when parsing takes only a few milliseconds. Shortening the interval reduces that delay but increases idle work across every transcript root.

Filesystem events change the trigger. The operating system tells the monitor that something under a watched root changed, and the monitor schedules a scan immediately. The event does not need to contain the application state. It only needs to wake the classifier.

macOS: one FSEvents stream over the known roots

The released macOS watcher covers three existing locations: Claude project transcripts, Codex sessions, and Claude Desktop session metadata. It requests file-level events with a 50 millisecond latency and no deferred delivery.

FSEventStreamCreate(
    nil,
    callback,
    &context,
    roots as CFArray,
    FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
    0.05,
    kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer
)

The callback filters for .jsonl files and Claude Desktop records whose names begin with local_. A relevant path calls one change handler. Parsing and state reduction remain outside the watcher.

Windows: one FileSystemWatcher per existing root

The Windows implementation watches the same categories of roots recursively. It subscribes to changed, created, and renamed events and asks for last-write, filename, and size notifications.

var watcher = new FileSystemWatcher(root) {
    IncludeSubdirectories = true,
    NotifyFilter = NotifyFilters.LastWrite
        | NotifyFilters.FileName
        | NotifyFilters.Size,
    InternalBufferSize = 64 * 1024,
};

A file save may produce more than one event. A rename can be part of an atomic replacement. The handler therefore treats every relevant event as a request to rescan current state, not as a command to apply one assumed transition.

Filesystem notifications can be incomplete

Native watchers are optimized notification channels. They are not an application ledger. FSEvents can coalesce related changes. FileSystemWatcher uses a finite buffer; a burst can overflow it. Roots can appear after startup, permission errors can block a watcher, and a process can replace a file rather than append to it.

Code that assumes one callback per transcript write will eventually hold stale state. Adding a larger buffer only changes the burst size required to expose the same design error.

Make every event idempotent and keep a sweep

The safe response to any notification is the same: inspect the current file set, read bounded evidence from changed candidates, and run the semantic classifier. Duplicate callbacks then repeat an idempotent read instead of duplicating an alarm.

On Windows, the watcher error callback schedules the same rescan used by a normal file event. The periodic poll remains active on both platforms. It covers roots that could not be watched and repairs state if a notification was coalesced, lost, or received while the app was suspended.

Coalesce work, not evidence

Several filesystem events can arrive for one logical write. A short scheduler-level coalescing window can combine scan requests. Do not discard transcript records based on that window. The parser still decides which ordered event is newest.

This separation keeps tuning safe. Changing event latency or scan scheduling cannot change the meaning of turn/completed, end_turn, or a user message.

Measure the recovery path

Record scan requests, scans actually run, changed files, bytes read, parse failures, watcher errors, and the age of the newest semantic event. Test three cases: a normal append, a burst that creates duplicate notifications, and a forced watcher failure followed by a successful poll.

The goal is bounded detection delay with no permanent stale state. A faster callback is useful only when recovery remains correct.

Product boundary

Agent Island v1.7.1 uses local filesystem evidence to update Claude Code and Codex status on macOS and Windows. The watcher does not read secrets from the event payload, upload session data to Agent Island, or decide whether generated code is correct. It wakes a local classifier.

Continue with efficient transcript monitoring, the shared turn-state contract, or working versus stalled state.

← All posts