diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 59d7e2b4f7e5..4ca6ddea9d9c 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -309,7 +309,11 @@ impl MessageProcessor { thread_manager .plugins_manager() .set_analytics_events_client(analytics_events_client.clone()); - let skills_watcher = SkillsWatcher::new(thread_manager.skills_service(), outgoing.clone()); + let skills_watcher = SkillsWatcher::new( + thread_manager.skills_service(), + &config.codex_home, + outgoing.clone(), + ); let pending_thread_unloads = Arc::new(Mutex::new(HashSet::new())); let thread_watch_manager = diff --git a/codex-rs/app-server/src/skills_watcher.rs b/codex-rs/app-server/src/skills_watcher.rs index 4852394fef0b..87661b904b15 100644 --- a/codex-rs/app-server/src/skills_watcher.rs +++ b/codex-rs/app-server/src/skills_watcher.rs @@ -15,7 +15,9 @@ use codex_file_watcher::Receiver; use codex_file_watcher::ThrottledWatchReceiver; use codex_file_watcher::WatchPath; use codex_file_watcher::WatchRegistration; +use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_skills::system_cache_root_dir; use codex_utils_absolute_path::AbsolutePathBuf; use tokio_util::sync::CancellationToken; use tokio_util::sync::DropGuard; @@ -36,6 +38,7 @@ pub(crate) struct SkillsWatcher { impl SkillsWatcher { pub(crate) fn new( skills_service: Arc, + codex_home: &AbsolutePathBuf, outgoing: Arc, ) -> Arc { let file_watcher = match FileWatcher::new() { @@ -48,7 +51,14 @@ impl SkillsWatcher { let (subscriber, rx) = file_watcher.add_subscriber(); let shutdown_token = CancellationToken::new(); let shutdown_drop_guard = shutdown_token.clone().drop_guard(); - Self::spawn_event_loop(rx, skills_service, outgoing, shutdown_token.child_token()); + let system_skills_root = system_cache_root_dir(codex_home); + Self::spawn_event_loop( + rx, + skills_service, + system_skills_root, + outgoing, + shutdown_token.child_token(), + ); Arc::new(Self { subscriber, runtime_extra_roots_registration: Mutex::new(WatchRegistration::default()), @@ -114,8 +124,9 @@ impl SkillsWatcher { .skill_roots_for_config(&skills_input, Some(environment.get_filesystem())) .await .into_iter() - // Plugin roots are invalidated by plugin lifecycle operations. - .filter(|root| root.plugin_identity.is_none()) + // Plugin roots have explicit lifecycle invalidation; generated system skills are + // installed before this watcher starts. + .filter(|root| root.plugin_identity.is_none() && root.scope != SkillScope::System) .map(|root| WatchPath { path: root.path.into_path_buf(), recursive: true, @@ -127,6 +138,7 @@ impl SkillsWatcher { fn spawn_event_loop( rx: Receiver, skills_service: Arc, + system_skills_root: AbsolutePathBuf, outgoing: Arc, shutdown_token: CancellationToken, ) { @@ -141,8 +153,16 @@ impl SkillsWatcher { _ = shutdown_token.cancelled() => break, event = rx.recv() => event, }; - if event.is_none() { + let Some(event) = event else { break; + }; + // The legacy user-skills root contains `.system` and is watched recursively. + if event + .paths + .iter() + .all(|path| path.starts_with(system_skills_root.as_path())) + { + continue; } skills_service.clear_cache(); outgoing