From 8550d6ab87f31acee7349fb67875e7c43552d15c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 9 Feb 2026 10:15:26 -0800 Subject: [PATCH] Fixed bug in file watcher that results in noise On some platforms, the "notify" file watcher library emits events for file opens and reads, not just file modifications or deletes. The previous implementation didn't take this into account. Furthermore, the `tracing.info!` call that I previously added was emitting a lot of logs. I had assumed incorrectly that `info` level logging was disabled by default, but it's apparently enabled for this crate. This is resulting in large logs (hundreds of MB) for some users. --- codex-rs/core/src/file_watcher.rs | 79 +++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/file_watcher.rs b/codex-rs/core/src/file_watcher.rs index 37fb1825084..427d5b6e4ed 100644 --- a/codex-rs/core/src/file_watcher.rs +++ b/codex-rs/core/src/file_watcher.rs @@ -11,6 +11,7 @@ use std::sync::RwLock; use std::time::Duration; use notify::Event; +use notify::EventKind; use notify::RecommendedWatcher; use notify::RecursiveMode; use notify::Watcher; @@ -19,7 +20,6 @@ use tokio::sync::broadcast; use tokio::sync::mpsc; use tokio::time::Instant; use tokio::time::sleep_until; -use tracing::info; use tracing::warn; use crate::config::Config; @@ -163,12 +163,6 @@ impl FileWatcher { res = raw_rx.recv() => { match res { Some(Ok(event)) => { - info!( - event_kind = ?event.kind, - event_paths = ?event.paths, - event_attrs = ?event.attrs, - "file watcher received filesystem event" - ); let skills_paths = classify_event(&event, &state); let now = Instant::now(); skills.add(skills_paths); @@ -245,6 +239,13 @@ impl FileWatcher { } fn classify_event(event: &Event, state: &RwLock) -> Vec { + if !matches!( + event.kind, + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) + ) { + return Vec::new(); + } + let mut skills_paths = Vec::new(); let skills_roots = match state.read() { Ok(state) => state.skills_roots.clone(), @@ -271,6 +272,11 @@ fn is_skills_path(path: &Path, roots: &HashSet) -> bool { mod tests { use super::*; use notify::EventKind; + use notify::event::AccessKind; + use notify::event::AccessMode; + use notify::event::CreateKind; + use notify::event::ModifyKind; + use notify::event::RemoveKind; use pretty_assertions::assert_eq; use tokio::time::timeout; @@ -278,8 +284,8 @@ mod tests { PathBuf::from(name) } - fn notify_event(paths: Vec) -> Event { - let mut event = Event::new(EventKind::Any); + fn notify_event(kind: EventKind, paths: Vec) -> Event { + let mut event = Event::new(kind); for path in paths { event = event.add_path(path); } @@ -327,10 +333,13 @@ mod tests { let state = RwLock::new(WatchState { skills_roots: HashSet::from([root.clone()]), }); - let event = notify_event(vec![ - root.join("demo/SKILL.md"), - path("/tmp/other/not-a-skill.txt"), - ]); + let event = notify_event( + EventKind::Create(CreateKind::Any), + vec![ + root.join("demo/SKILL.md"), + path("/tmp/other/not-a-skill.txt"), + ], + ); let classified = classify_event(&event, &state); assert_eq!(classified, vec![root.join("demo/SKILL.md")]); @@ -343,11 +352,14 @@ mod tests { let state = RwLock::new(WatchState { skills_roots: HashSet::from([root_a.clone(), root_b.clone()]), }); - let event = notify_event(vec![ - root_a.join("alpha/SKILL.md"), - path("/tmp/skills-extra/not-under-skills.txt"), - root_b.join("beta/SKILL.md"), - ]); + let event = notify_event( + EventKind::Modify(ModifyKind::Any), + vec![ + root_a.join("alpha/SKILL.md"), + path("/tmp/skills-extra/not-under-skills.txt"), + root_b.join("beta/SKILL.md"), + ], + ); let classified = classify_event(&event, &state); assert_eq!( @@ -356,6 +368,27 @@ mod tests { ); } + #[test] + fn classify_event_ignores_non_mutating_event_kinds() { + let root = path("/tmp/skills"); + let state = RwLock::new(WatchState { + skills_roots: HashSet::from([root.clone()]), + }); + let path = root.join("demo/SKILL.md"); + + let access_event = notify_event( + EventKind::Access(AccessKind::Open(AccessMode::Any)), + vec![path.clone()], + ); + assert_eq!(classify_event(&access_event, &state), Vec::::new()); + + let any_event = notify_event(EventKind::Any, vec![path.clone()]); + assert_eq!(classify_event(&any_event, &state), Vec::::new()); + + let other_event = notify_event(EventKind::Other, vec![path]); + assert_eq!(classify_event(&other_event, &state), Vec::::new()); + } + #[test] fn register_skills_root_dedupes_state_entries() { let watcher = FileWatcher::noop(); @@ -382,7 +415,10 @@ mod tests { watcher.spawn_event_loop(raw_rx, Arc::clone(&watcher.state), tx); raw_tx - .send(Ok(notify_event(vec![root.join("a/SKILL.md")]))) + .send(Ok(notify_event( + EventKind::Create(CreateKind::File), + vec![root.join("a/SKILL.md")], + ))) .expect("send first event"); let first = timeout(Duration::from_secs(2), rx.recv()) .await @@ -396,7 +432,10 @@ mod tests { ); raw_tx - .send(Ok(notify_event(vec![root.join("b/SKILL.md")]))) + .send(Ok(notify_event( + EventKind::Remove(RemoveKind::File), + vec![root.join("b/SKILL.md")], + ))) .expect("send second event"); drop(raw_tx);