mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary - Preserve raw plugin skill-root snapshots in the matching loaded-plugin cache entry, keyed by the effective plugin root identity including namespace. - Pass those snapshots through `SkillsLoadInput` as an optional preload, so session startup reuses plugin parsing while ordinary skill loads pass `None`. - Keep plugin skill loading cohesive: the existing loaders accept the optional snapshots directly, and uncached or marketplace-detail paths do not create a cache. ## Why Plugin discovery already parses plugin skills to determine available capabilities. Cold session startup then scanned and parsed the same roots again while building the skills snapshot. This solves the same duplicate-work problem as #28623 while keeping ownership narrow: `PluginsManager` creates and owns `PluginSkillSnapshots` only for its loaded-plugin cache entry; `SkillsService` consumes an optional clone. Entry replacement or clearing naturally drops the snapshots, with no separate generation, capacity policy, or watcher coupling. ## Validation - `cargo clippy -p codex-core-skills --all-targets -- -D warnings` - `just test -p codex-core-plugins skills_service_reuses_skills_parsed_during_plugin_load` - `just test -p codex-core-skills namespaces_plugin_skills_using_provided_namespace` - `just fmt`
157 lines
5.5 KiB
Rust
157 lines
5.5 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::time::Duration;
|
|
|
|
use crate::outgoing_message::OutgoingMessageSender;
|
|
use codex_app_server_protocol::ServerNotification;
|
|
use codex_app_server_protocol::SkillsChangedNotification;
|
|
use codex_core::ThreadManager;
|
|
use codex_core::config::Config;
|
|
use codex_core::skills::SkillsLoadInput;
|
|
use codex_core::skills::SkillsService;
|
|
use codex_file_watcher::FileWatcher;
|
|
use codex_file_watcher::FileWatcherSubscriber;
|
|
use codex_file_watcher::Receiver;
|
|
use codex_file_watcher::ThrottledWatchReceiver;
|
|
use codex_file_watcher::WatchPath;
|
|
use codex_file_watcher::WatchRegistration;
|
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tokio_util::sync::DropGuard;
|
|
use tracing::warn;
|
|
|
|
#[cfg(not(test))]
|
|
const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_secs(10);
|
|
#[cfg(test)]
|
|
const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_millis(50);
|
|
|
|
pub(crate) struct SkillsWatcher {
|
|
subscriber: FileWatcherSubscriber,
|
|
runtime_extra_roots_registration: Mutex<WatchRegistration>,
|
|
shutdown_token: CancellationToken,
|
|
_shutdown_drop_guard: DropGuard,
|
|
}
|
|
|
|
impl SkillsWatcher {
|
|
pub(crate) fn new(
|
|
skills_service: Arc<SkillsService>,
|
|
outgoing: Arc<OutgoingMessageSender>,
|
|
) -> Arc<Self> {
|
|
let file_watcher = match FileWatcher::new() {
|
|
Ok(file_watcher) => Arc::new(file_watcher),
|
|
Err(err) => {
|
|
warn!("failed to initialize skills file watcher: {err}");
|
|
Arc::new(FileWatcher::noop())
|
|
}
|
|
};
|
|
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());
|
|
Arc::new(Self {
|
|
subscriber,
|
|
runtime_extra_roots_registration: Mutex::new(WatchRegistration::default()),
|
|
shutdown_token,
|
|
_shutdown_drop_guard: shutdown_drop_guard,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn shutdown(&self) {
|
|
self.shutdown_token.cancel();
|
|
}
|
|
|
|
pub(crate) fn register_runtime_extra_roots(&self, extra_roots: &[AbsolutePathBuf]) {
|
|
let roots = extra_roots
|
|
.iter()
|
|
.map(|root| WatchPath {
|
|
path: root.clone().into_path_buf(),
|
|
recursive: true,
|
|
})
|
|
.collect();
|
|
let registration = self.subscriber.register_paths(roots);
|
|
let mut guard = self
|
|
.runtime_extra_roots_registration
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
*guard = registration;
|
|
}
|
|
|
|
pub(crate) async fn register_thread_config(
|
|
&self,
|
|
config: &Config,
|
|
thread_manager: &ThreadManager,
|
|
environments: &[TurnEnvironmentSelection],
|
|
) -> WatchRegistration {
|
|
let Some(environment_selection) = environments.first() else {
|
|
return WatchRegistration::default();
|
|
};
|
|
let Some(environment) = thread_manager
|
|
.environment_manager()
|
|
.get_environment(&environment_selection.environment_id)
|
|
else {
|
|
warn!(
|
|
"failed to register skills watcher for unknown environment `{}`",
|
|
environment_selection.environment_id
|
|
);
|
|
return WatchRegistration::default();
|
|
};
|
|
if environment.is_remote() {
|
|
return WatchRegistration::default();
|
|
}
|
|
|
|
let plugins_input = config.plugins_config_input();
|
|
let plugins_manager = thread_manager.plugins_manager();
|
|
let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await;
|
|
let skills_input = SkillsLoadInput::new(
|
|
config.cwd.clone(),
|
|
plugin_outcome.effective_plugin_skill_roots(),
|
|
config.config_layer_stack.clone(),
|
|
config.bundled_skills_enabled(),
|
|
);
|
|
let roots = thread_manager
|
|
.skills_service()
|
|
.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_id.is_none())
|
|
.map(|root| WatchPath {
|
|
path: root.path.into_path_buf(),
|
|
recursive: true,
|
|
})
|
|
.collect();
|
|
self.subscriber.register_paths(roots)
|
|
}
|
|
|
|
fn spawn_event_loop(
|
|
rx: Receiver,
|
|
skills_service: Arc<SkillsService>,
|
|
outgoing: Arc<OutgoingMessageSender>,
|
|
shutdown_token: CancellationToken,
|
|
) {
|
|
let mut rx = ThrottledWatchReceiver::new(rx, WATCHER_THROTTLE_INTERVAL);
|
|
let Ok(handle) = tokio::runtime::Handle::try_current() else {
|
|
warn!("skills watcher listener skipped: no Tokio runtime available");
|
|
return;
|
|
};
|
|
handle.spawn(async move {
|
|
loop {
|
|
let event = tokio::select! {
|
|
_ = shutdown_token.cancelled() => break,
|
|
event = rx.recv() => event,
|
|
};
|
|
if event.is_none() {
|
|
break;
|
|
}
|
|
skills_service.clear_cache();
|
|
outgoing
|
|
.send_server_notification(ServerNotification::SkillsChanged(
|
|
SkillsChangedNotification {},
|
|
))
|
|
.await;
|
|
}
|
|
});
|
|
}
|
|
}
|