app-server fs/watch misses append updates to active rollout JSONL on macOS
What issue are you seeing?
On macOS, app-server fs/watch does not emit fs/changed while an active Codex rollout JSONL is being appended to.
The rollout file size and modification time continue changing as reasoning, tool-call, and output items are written, but the app-server connection receives no corresponding fs/changed notification. A notification may appear around a user-message or turn boundary, but incremental turn items do not reliably trigger one.
This prevents clients that use the file notification as a signal to call thread/read from updating the active turn in real time.
What steps can reproduce the bug?
- Start
codex app-serverover stdio and sendinitialize. - Start or resume a thread and locate its active rollout JSONL under the Codex sessions directory.
- Subscribe to the exact absolute file path:
{ "method": "fs/watch", "id": 44, "params": {
"watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1",
"path": "/absolute/path/to/rollout.jsonl"
} }
- Continue the turn so Codex appends reasoning, tool-call, command-output, and assistant-output records to the same file.
- Observe that the file size and mtime change, but no
fs/changednotification is emitted for those appends.
Control checks from the same machine:
- The same app-server watcher emits
fs/changedfor a newly created file in a watched temporary directory, so the subscription and connection are working. fswatchusing its default macOS backend also misses the active rollout appends.fswatch -m kqueue_monitoron the same rollout file reports every append immediately.
The FSEvents/kqueue comparison is diagnostic evidence, not a confirmed root cause.
What is the expected behavior?
After the watcher debounce interval, each modification or batch of modifications to the watched file should emit:
{ "method": "fs/changed", "params": {
"watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1",
"changedPaths": ["/absolute/path/to/rollout.jsonl"]
} }
This should work for append-only writes while the writer keeps the file open, not only for create, replace, rename, or turn-boundary activity.
Additional information
- Codex CLI:
0.148.0(current latest release at time of report) - Platform:
Darwin 25.5.0 arm64 arm - macOS:
26.5.2(25F84) - Architecture: Apple Silicon
- Installation: Homebrew
codex doctor --json: app-server check is OK; the unrelated overall failure is only because the noninteractive diagnostic shell hasTERM=dumb.- The app-server documentation says watching a file emits
fs/changedfor that path. The observed behavior differs specifically for sustained append activity on the active rollout file.
3 Comments
Source trace on current main (@67b2c8c) — the codex-side pipeline does not drop Modify events, which points the finger at the watcher backend itself, consistent with your fswatch A/B.
How
fs/watchis implemented:FsWatchManager::watchregisters the exact requested path non-recursively and forwards events after a debounce (codex-rs/app-server/src/fs_watch.rs:76–148);fs/watchdispatch atcodex-rs/app-server/src/message_processor.rs:1102.notify::recommended_watcher(...)withConfig::default()(codex-rs/file-watcher/src/lib.rs:379–392),notify = "8.2.0"with default features (codex-rs/Cargo.toml:366). With default features,RecommendedWatcheron macOS is the FSEvents backend (macos_fseventis the default feature;macos_kqueueis opt-in), file-level streams, latency 0.0, NoDefer, since-now.is_mutating_event,file-watcher/src/lib.rs:761–766) — so anItemModifiedfor an append would pass; there is no create/rename-only filtering. The 200 ms debounce (fs_watch.rs:27) coalesces paths until a deadline but never drops them.And the writer is exactly the "append through a long-lived open fd" class:
open_rollout_for_appendkeeps one append-mode file handle for the session and each item iswrite_all+flushon that fd (codex-rs/rollout/src/recorder.rs:1896–1919; production pathcodex-rs/thread-store/src/local/live_writer.rs:354–362). Normal turns do not rewrite/rename the rollout.Hypothesis (labelled, but it matches all three of your controls): the FSEvents stream does not reliably deliver
kFSEventStreamEventItemModifiedfor appends made through a persistently open writer fd, while create/rename events are delivered — which is whyfswatch(FSEvents) misses the appends,fswatch -m kqueue_monitorcatches them, and notifications appear around turn boundaries (boundary-adjacent operations FSEvents does report: materialize/rename from compression, writer re-open, mtime touches) rather than for the appends themselves.Direction that would cover it: switch the macOS backend to kqueue — either enable
notify/macos_kqueueincodex-rs/Cargo.toml:366(thenRecommendedWatcherresolves to the kqueue watcher, whoseEVFILT_VNODEwithNOTE_WRITE/NOTE_EXTENDcatches appends on the registered file), or construct it explicitly inFileWatcher::new. The per-file watch design already ref-counts exact paths, so fd cost scales with subscription count. Watching the parent directory instead would not fix it by itself, per your FSEvents control.Coverage note: the existing fs/watch tests only exercise file creation and atomic replace (
app-server/tests/suite/v2/fs.rs:753), so there is no regression coverage for the append-with-open-fd case.I verified the proposed fix direction against
notify 8.2.0and it checks out — happy to implement it.Details that confirm the approach:
notify 8.2.0declares macOS-gated optional depskqueue(1.1.1) andmio(1.0,os-ext) under[target."cfg(target_os=\"macos\")".dependencies](notify/Cargo.toml:103-110), so enabling themacos_kqueuefeature is sufficient on macOS — no extra non-macOS crates leak in.RecommendedWatcherresolution:all(target_os = "macos", feature = "macos_kqueue")→KqueueWatcher(notify/src/lib.rs:419-421), overriding the default FSEvents mapping (lib.rs:407-408). Kqueue'sEVFILT_VNODE+NOTE_WRITE/NOTE_EXTENDdelivers the append-through-open-fd modifications that FSEvents misses.file-watcher/src/lib.rs) keeps the fd cost proportional to subscriptions; still worth noting that recursive directory watches will also move to kqueue with a global feature switch (kqueue watches each subdirectory individually, unlike FSEvents' single stream) — I'll validate the existingfs/watchsuite for regressions.Planned change (small, one concern):
notify/macos_kqueuefor thecodex-file-watchercrate.codex-file-watcherand app-serverfs/watchtest suites.I'd like to work on this.
I implemented the kqueue switch and have it ready in my fork: https://github.com/argszero/codex/tree/fix/fs-watch-kqueue (commit
de27bc4c, based on currentmain).What the change does:
codex-rs/file-watcher/Cargo.toml:notify = { workspace = true, features = ["macos_kqueue"] }. With this feature,notify::recommended_watcherresolves toKqueueWatcheron macOS (EVFILT_VNODE+NOTE_WRITE/NOTE_EXTEND), which delivers the append-through-open-fd modifications that FSEvents misses. The feature only pulls macOS-gated optional deps (kqueue,mioundercfg(target_os = "macos")) and is a no-op on other platforms; nothing else in the workspace consumesnotify.live_watcher_reports_appends_through_open_fdincodex-rs/file-watcher/src/file_watcher_tests.rs: watches an existing file, appends multiple records through a persistent open fd (mirroringopen_rollout_for_append's writer pattern), and asserts the change event is delivered with the watched path — the case that previously had no coverage.Verified on macOS arm64:
cargo test -p codex-file-watcher22/22 pass (new test included),cargo clippy --all-targetsandcargo fmt --checkclean.Cargo.lockandMODULE.bazel.lockare unchanged.I attempted to open this as a PR but GitHub rejected PR creation for this repository from an external account (GraphQL
CreatePullRequestpermission denied, REST 404), which I understand is the platform enforcement of the invitation-only contribution policy. Since you outlined the direction in this thread, I'd be glad to open the PR if you can invite the contribution — or if you'd prefer to take the patch from here, the branch is ready for you to pull from. Happy to adjust the approach (e.g. explicitKqueueWatcherconstruction instead of the feature switch, or an app-server-level test) if you'd like it done differently.