mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Keep multi-workspace skill listings consistent (#37812)
## Why A single `skills/list` request should use one view of shared skill roots across all requested workspaces while still applying each workspace's skill rules. ## What changed - Add a request-scoped host skills view that reuses non-plugin root snapshots across `cwd` entries without persisting them across requests. - Resolve user-scoped plugin skill roots once per request, then apply workspace skill configuration separately for each `cwd`. - Make `forceReload` refresh plugin roots before building the request-scoped view so subsequent entries see the refreshed skills. ## Testing Expanded `skills/list` and host skills service tests to cover multiple ordered workspaces, per-workspace skill rules, request-local snapshots, cached results, and forced plugin-root refreshes. GitOrigin-RevId: eedd3a4c3e213b7f30df6cacd3adf23ba2967437
This commit is contained in:
1 parent
34ecac1f2b
commit
1c042dd4d8
9 files changed
+429
-140
No files matched your search
@@ -514,17 +514,30 @@ impl CatalogRequestProcessor {
|
|||||||
plugins_manager.clear_cache();
|
plugins_manager.clear_cache();
|
||||||
skills_service.clear_cache();
|
skills_service.clear_cache();
|
||||||
}
|
}
|
||||||
|
// Plugin configuration is user-scoped; workspace skill rules are applied below.
|
||||||
|
let (effective_skill_roots, plugin_skill_snapshots) = if workspace_codex_plugins_enabled {
|
||||||
|
let plugins_input = config.plugins_config_input();
|
||||||
|
let plugins = plugins_manager.plugins_for_config(&plugins_input).await;
|
||||||
|
(
|
||||||
|
plugins.effective_plugin_skill_roots(),
|
||||||
|
plugins_manager.plugin_skill_snapshots_for_config(&plugins_input),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), None)
|
||||||
|
};
|
||||||
let fs = self
|
let fs = self
|
||||||
.thread_manager
|
.thread_manager
|
||||||
.environment_manager()
|
.environment_manager()
|
||||||
.default_environment()
|
.default_environment()
|
||||||
.map(|environment| environment.get_filesystem());
|
.map(|environment| environment.get_filesystem());
|
||||||
|
let skills_request = skills_service.for_request();
|
||||||
let mut data = futures::stream::iter(cwds.into_iter().enumerate())
|
let mut data = futures::stream::iter(cwds.into_iter().enumerate())
|
||||||
.map(|(index, cwd)| {
|
.map(|(index, cwd)| {
|
||||||
let config = &config;
|
let config = &config;
|
||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
let plugins_manager = &plugins_manager;
|
let skills_request = &skills_request;
|
||||||
let skills_service = &skills_service;
|
let effective_skill_roots = effective_skill_roots.clone();
|
||||||
|
let plugin_skill_snapshots = plugin_skill_snapshots.clone();
|
||||||
async move {
|
async move {
|
||||||
let (cwd_abs, config_layer_stack) = match self.resolve_cwd_config(&cwd).await {
|
let (cwd_abs, config_layer_stack) = match self.resolve_cwd_config(&cwd).await {
|
||||||
Ok(resolved) => resolved,
|
Ok(resolved) => resolved,
|
||||||
@@ -543,31 +556,6 @@ impl CatalogRequestProcessor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (effective_skill_roots, plugin_skill_snapshots) =
|
|
||||||
if workspace_codex_plugins_enabled {
|
|
||||||
let plugins_input = config.plugins_config_input();
|
|
||||||
if config_layer_stack == plugins_input.config_layer_stack {
|
|
||||||
let plugins =
|
|
||||||
plugins_manager.plugins_for_config(&plugins_input).await;
|
|
||||||
(
|
|
||||||
plugins.effective_plugin_skill_roots(),
|
|
||||||
plugins_manager
|
|
||||||
.plugin_skill_snapshots_for_config(&plugins_input),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
plugins_manager
|
|
||||||
.effective_skill_roots_for_layer_stack(
|
|
||||||
&config_layer_stack,
|
|
||||||
&plugins_input,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(Vec::new(), None)
|
|
||||||
};
|
|
||||||
let skills_input = codex_core::skills::HostSkillsLoadInput::new(
|
let skills_input = codex_core::skills::HostSkillsLoadInput::new(
|
||||||
cwd_abs.clone(),
|
cwd_abs.clone(),
|
||||||
effective_skill_roots,
|
effective_skill_roots,
|
||||||
@@ -575,7 +563,7 @@ impl CatalogRequestProcessor {
|
|||||||
config.bundled_skills_enabled(),
|
config.bundled_skills_enabled(),
|
||||||
)
|
)
|
||||||
.with_plugin_skill_snapshots(plugin_skill_snapshots);
|
.with_plugin_skill_snapshots(plugin_skill_snapshots);
|
||||||
let snapshot = skills_service
|
let snapshot = skills_request
|
||||||
.snapshot_for_cwd(&skills_input, force_reload, fs)
|
.snapshot_for_cwd(&skills_input, force_reload, fs)
|
||||||
.await;
|
.await;
|
||||||
let outcome = snapshot.outcome();
|
let outcome = snapshot.outcome();
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ use codex_app_server_protocol::SkillsListResponse;
|
|||||||
use codex_app_server_protocol::ThreadStartParams;
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
use codex_app_server_protocol::ThreadStartResponse;
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
use codex_config::types::AuthCredentialsStoreMode;
|
use codex_config::types::AuthCredentialsStoreMode;
|
||||||
|
use codex_core::config::set_project_trust_level;
|
||||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||||
|
use codex_exec_server::CreateDirectoryOptions;
|
||||||
|
use codex_protocol::config_types::TrustLevel;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use core_test_support::skip_if_remote;
|
use core_test_support::skip_if_remote;
|
||||||
|
use core_test_support::skip_if_wine_exec;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
@@ -777,37 +782,194 @@ async fn skills_list_accepts_relative_cwds() -> Result<()> {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
|
async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
|
||||||
|
skip_if_wine_exec!(
|
||||||
|
Ok(()),
|
||||||
|
"skills/list currently requires host-native cwd paths for workspace config"
|
||||||
|
);
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
let first_cwd = TempDir::new()?;
|
let first_cwd = TempDir::new()?;
|
||||||
let second_cwd = TempDir::new()?;
|
let second_cwd = TempDir::new()?;
|
||||||
|
write_skill(&codex_home, "shared-skill")?;
|
||||||
|
write_cached_local_curated_plugin_with_skill(codex_home.path())?;
|
||||||
|
std::fs::write(
|
||||||
|
codex_home.path().join("config.toml"),
|
||||||
|
r#"[features]
|
||||||
|
plugins = true
|
||||||
|
|
||||||
|
[plugins."google-calendar@openai-curated"]
|
||||||
|
enabled = true
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for (cwd, plugin_enabled) in [(first_cwd.path(), true), (second_cwd.path(), false)] {
|
||||||
|
std::fs::create_dir_all(cwd.join(".git"))?;
|
||||||
|
std::fs::create_dir_all(cwd.join(".codex"))?;
|
||||||
|
std::fs::write(
|
||||||
|
cwd.join(".codex/config.toml"),
|
||||||
|
format!("[plugins.\"google-calendar@openai-curated\"]\nenabled = {plugin_enabled}\n"),
|
||||||
|
)?;
|
||||||
|
set_project_trust_level(codex_home.path(), cwd, TrustLevel::Trusted)?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut mcp = TestAppServer::builder()
|
let mut mcp = TestAppServer::builder()
|
||||||
.with_codex_home(codex_home.path())
|
.with_codex_home(codex_home.path())
|
||||||
.without_auto_env()
|
|
||||||
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
|
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let request_id = mcp
|
let file_system = mcp.auto_env()?.environment().get_filesystem();
|
||||||
.send_skills_list_request(SkillsListParams {
|
for (cwd, name) in [
|
||||||
cwds: vec![
|
(first_cwd.path(), "first-project-skill"),
|
||||||
first_cwd.path().to_path_buf(),
|
(second_cwd.path(), "second-project-skill"),
|
||||||
second_cwd.path().to_path_buf(),
|
] {
|
||||||
],
|
let cwd = AbsolutePathBuf::try_from(cwd)?;
|
||||||
force_reload: true,
|
let git_dir = PathUri::from_abs_path(&cwd.join(".git"));
|
||||||
})
|
let skill_dir = PathUri::from_abs_path(&cwd.join(".agents/skills").join(name));
|
||||||
|
for directory in [&git_dir, &skill_dir] {
|
||||||
|
file_system
|
||||||
|
.create_directory(
|
||||||
|
directory,
|
||||||
|
CreateDirectoryOptions { recursive: true },
|
||||||
|
/*sandbox*/ None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
file_system
|
||||||
|
.write_file(
|
||||||
|
&skill_dir.join("SKILL.md")?,
|
||||||
|
format!("---\nname: {name}\ndescription: {name}\n---\n").into_bytes(),
|
||||||
|
/*sandbox*/ None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (request_index, force_reload) in [false, false, true].into_iter().enumerate() {
|
||||||
|
if request_index == 1 {
|
||||||
|
write_skill(&codex_home, "new-skill")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_id = mcp
|
||||||
|
.send_skills_list_request(SkillsListParams {
|
||||||
|
cwds: vec![
|
||||||
|
first_cwd.path().to_path_buf(),
|
||||||
|
second_cwd.path().to_path_buf(),
|
||||||
|
],
|
||||||
|
force_reload,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let SkillsListResponse { data } =
|
||||||
|
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||||
|
assert_eq!(data.len(), 2);
|
||||||
|
assert_eq!(data[0].cwd, first_cwd.path());
|
||||||
|
assert_eq!(data[1].cwd, second_cwd.path());
|
||||||
|
for (entry, project_skill) in data
|
||||||
|
.iter()
|
||||||
|
.zip(["first-project-skill", "second-project-skill"])
|
||||||
|
{
|
||||||
|
assert_eq!(entry.errors, Vec::new());
|
||||||
|
assert!(
|
||||||
|
entry
|
||||||
|
.skills
|
||||||
|
.iter()
|
||||||
|
.any(|skill| skill.name == "shared-skill")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
entry
|
||||||
|
.skills
|
||||||
|
.iter()
|
||||||
|
.filter(|skill| {
|
||||||
|
skill.name.ends_with("-project-skill")
|
||||||
|
|| skill.name.starts_with("google-calendar:")
|
||||||
|
})
|
||||||
|
.map(|skill| skill.name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![project_skill, "google-calendar:meeting-prep"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
entry.skills.iter().any(|skill| skill.name == "new-skill"),
|
||||||
|
force_reload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn skills_list_force_reload_refreshes_cached_plugin_roots() -> Result<()> {
|
||||||
|
skip_if_wine_exec!(
|
||||||
|
Ok(()),
|
||||||
|
"skills/list currently requires host-native cwd paths for workspace config"
|
||||||
|
);
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let first_cwd = TempDir::new()?;
|
||||||
|
let second_cwd = TempDir::new()?;
|
||||||
|
write_cached_local_curated_plugin_with_skill(codex_home.path())?;
|
||||||
|
std::fs::write(
|
||||||
|
codex_home.path().join("config.toml"),
|
||||||
|
r#"[features]
|
||||||
|
plugins = true
|
||||||
|
|
||||||
|
[plugins."google-calendar@openai-curated"]
|
||||||
|
enabled = true
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut mcp = TestAppServer::builder()
|
||||||
|
.with_codex_home(codex_home.path())
|
||||||
|
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let SkillsListResponse { data } =
|
let file_system = mcp.auto_env()?.environment().get_filesystem();
|
||||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
for cwd in [first_cwd.path(), second_cwd.path()] {
|
||||||
assert_eq!(
|
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::try_from(cwd)?);
|
||||||
data.iter()
|
file_system
|
||||||
.map(|entry| entry.cwd.clone())
|
.create_directory(
|
||||||
.collect::<Vec<_>>(),
|
&cwd.join(".git")?,
|
||||||
vec![
|
CreateDirectoryOptions { recursive: true },
|
||||||
first_cwd.path().to_path_buf(),
|
/*sandbox*/ None,
|
||||||
second_cwd.path().to_path_buf(),
|
)
|
||||||
]
|
.await?;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
for (cwd, force_reload, expected_skill) in [
|
||||||
|
(first_cwd.path(), false, "google-calendar:meeting-prep"),
|
||||||
|
(first_cwd.path(), true, "google-calendar:refreshed-skill"),
|
||||||
|
(second_cwd.path(), false, "google-calendar:refreshed-skill"),
|
||||||
|
] {
|
||||||
|
if force_reload {
|
||||||
|
let plugin_root = codex_home
|
||||||
|
.path()
|
||||||
|
.join("plugins/cache/openai-curated/google-calendar/local");
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join(".codex-plugin/plugin.json"),
|
||||||
|
r#"{"name":"google-calendar","skills":"./replacement-skills"}"#,
|
||||||
|
)?;
|
||||||
|
let skill_dir = plugin_root.join("replacement-skills/refreshed-skill");
|
||||||
|
std::fs::create_dir_all(&skill_dir)?;
|
||||||
|
std::fs::write(
|
||||||
|
skill_dir.join("SKILL.md"),
|
||||||
|
"---\nname: refreshed-skill\ndescription: refreshed skill\n---\n",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_id = mcp
|
||||||
|
.send_skills_list_request(SkillsListParams {
|
||||||
|
cwds: vec![cwd.to_path_buf()],
|
||||||
|
force_reload,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let SkillsListResponse { data } =
|
||||||
|
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||||
|
assert_eq!(
|
||||||
|
data[0]
|
||||||
|
.skills
|
||||||
|
.iter()
|
||||||
|
.filter(|skill| skill.name.starts_with("google-calendar:"))
|
||||||
|
.map(|skill| skill.name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![expected_skill]
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -787,28 +787,6 @@ impl PluginsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load plugins for a config layer stack without touching the plugins cache.
|
|
||||||
pub async fn plugins_for_layer_stack(
|
|
||||||
&self,
|
|
||||||
config_layer_stack: &ConfigLayerStack,
|
|
||||||
config: &PluginsConfigInput,
|
|
||||||
) -> PluginLoadOutcome {
|
|
||||||
if !config.plugins_enabled {
|
|
||||||
return PluginLoadOutcome::default();
|
|
||||||
}
|
|
||||||
let plugins = load_plugins_from_layer_stack(
|
|
||||||
config_layer_stack,
|
|
||||||
self.remote_installed_plugins_snapshot(),
|
|
||||||
&self.store,
|
|
||||||
/*plugin_skill_snapshots*/ None,
|
|
||||||
self.restriction_product,
|
|
||||||
self.remote_global_catalog_active(config),
|
|
||||||
self.skill_root_loader.as_ref(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve plugin hooks for a config layer stack without loading other plugin capabilities.
|
/// Resolve plugin hooks for a config layer stack without loading other plugin capabilities.
|
||||||
pub async fn plugin_hooks_for_layer_stack(
|
pub async fn plugin_hooks_for_layer_stack(
|
||||||
&self,
|
&self,
|
||||||
@@ -830,17 +808,6 @@ impl PluginsManager {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve plugin skill roots for a config layer stack without touching the plugins cache.
|
|
||||||
pub async fn effective_skill_roots_for_layer_stack(
|
|
||||||
&self,
|
|
||||||
config_layer_stack: &ConfigLayerStack,
|
|
||||||
config: &PluginsConfigInput,
|
|
||||||
) -> Vec<PluginSkillRoot> {
|
|
||||||
self.plugins_for_layer_stack(config_layer_stack, config)
|
|
||||||
.await
|
|
||||||
.effective_plugin_skill_roots()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cached_loaded_plugins(&self, key: &PluginLoadCacheKey) -> Option<Vec<LoadedPlugin>> {
|
fn cached_loaded_plugins(&self, key: &PluginLoadCacheKey) -> Option<Vec<LoadedPlugin>> {
|
||||||
self.loaded_plugins_cache
|
self.loaded_plugins_cache
|
||||||
.read()
|
.read()
|
||||||
|
|||||||
@@ -5183,6 +5183,7 @@ async fn new_default_turn_uses_config_aware_skills_for_role_overrides() {
|
|||||||
let parent_snapshot = session
|
let parent_snapshot = session
|
||||||
.services
|
.services
|
||||||
.skills_service
|
.skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&crate::skills_load_input_from_config(&parent_config, Vec::new()),
|
&crate::skills_load_input_from_config(&parent_config, Vec::new()),
|
||||||
/*force_reload*/ true,
|
/*force_reload*/ true,
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ use crate::HostSkillsSnapshot;
|
|||||||
use crate::SkillLoadOutcome;
|
use crate::SkillLoadOutcome;
|
||||||
use crate::host_roots::resolve_skill_roots;
|
use crate::host_roots::resolve_skill_roots;
|
||||||
use crate::loader::HostSkillRoot;
|
use crate::loader::HostSkillRoot;
|
||||||
|
use crate::loader::HostSkillRootSnapshot;
|
||||||
use crate::loader::MAX_CONCURRENT_ROOT_SCANS;
|
use crate::loader::MAX_CONCURRENT_ROOT_SCANS;
|
||||||
use crate::loader::load_and_merge_host_skill_roots;
|
use crate::loader::load_and_merge_host_skill_roots;
|
||||||
|
use crate::loader::load_and_merge_host_skill_roots_with_request_snapshots;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HostSkillsLoadInput {
|
pub struct HostSkillsLoadInput {
|
||||||
@@ -84,6 +86,33 @@ pub struct HostSkillsService {
|
|||||||
root_scan_slots: Arc<Semaphore>,
|
root_scan_slots: Arc<Semaphore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) type RequestSkillRootSnapshots =
|
||||||
|
RwLock<HashMap<ConfigSkillRootCacheKey, Arc<OnceCell<HostSkillRootSnapshot>>>>;
|
||||||
|
|
||||||
|
/// Shares host-root scans between workspaces for the lifetime of one request.
|
||||||
|
pub struct HostSkillsRequest<'a> {
|
||||||
|
service: &'a HostSkillsService,
|
||||||
|
root_snapshots: RequestSkillRootSnapshots,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostSkillsRequest<'_> {
|
||||||
|
pub async fn snapshot_for_cwd(
|
||||||
|
&self,
|
||||||
|
input: &HostSkillsLoadInput,
|
||||||
|
force_reload: bool,
|
||||||
|
fs: Option<Arc<dyn ExecutorFileSystem>>,
|
||||||
|
) -> HostSkillsSnapshot {
|
||||||
|
self.service
|
||||||
|
.snapshot_for_cwd_with_root_snapshots(
|
||||||
|
input,
|
||||||
|
force_reload,
|
||||||
|
fs,
|
||||||
|
Some(&self.root_snapshots),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl HostSkillsService {
|
impl HostSkillsService {
|
||||||
pub fn new(codex_home: AbsolutePathBuf, bundled_skills_enabled: bool) -> Self {
|
pub fn new(codex_home: AbsolutePathBuf, bundled_skills_enabled: bool) -> Self {
|
||||||
Self::new_with_restriction_product(codex_home, bundled_skills_enabled, Some(Product::Codex))
|
Self::new_with_restriction_product(codex_home, bundled_skills_enabled, Some(Product::Codex))
|
||||||
@@ -110,6 +139,14 @@ impl HostSkillsService {
|
|||||||
service
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a request-local view without changing persistent plugin snapshots.
|
||||||
|
pub fn for_request(&self) -> HostSkillsRequest<'_> {
|
||||||
|
HostSkillsRequest {
|
||||||
|
service: self,
|
||||||
|
root_snapshots: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_extra_roots(&self, extra_roots: Vec<AbsolutePathBuf>) {
|
pub fn set_extra_roots(&self, extra_roots: Vec<AbsolutePathBuf>) {
|
||||||
{
|
{
|
||||||
let mut roots = self
|
let mut roots = self
|
||||||
@@ -155,6 +192,7 @@ impl HostSkillsService {
|
|||||||
&skill_config_rules,
|
&skill_config_rules,
|
||||||
cache_key,
|
cache_key,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
|
/*request_root_snapshots*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -181,11 +219,12 @@ impl HostSkillsService {
|
|||||||
roots
|
roots
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn snapshot_for_cwd(
|
async fn snapshot_for_cwd_with_root_snapshots(
|
||||||
&self,
|
&self,
|
||||||
input: &HostSkillsLoadInput,
|
input: &HostSkillsLoadInput,
|
||||||
force_reload: bool,
|
force_reload: bool,
|
||||||
fs: Option<Arc<dyn ExecutorFileSystem>>,
|
fs: Option<Arc<dyn ExecutorFileSystem>>,
|
||||||
|
request_root_snapshots: Option<&RequestSkillRootSnapshots>,
|
||||||
) -> HostSkillsSnapshot {
|
) -> HostSkillsSnapshot {
|
||||||
let bundled_skills_enabled = bundled_skills_enabled_from_stack(&input.config_layer_stack);
|
let bundled_skills_enabled = bundled_skills_enabled_from_stack(&input.config_layer_stack);
|
||||||
if bundled_skills_enabled {
|
if bundled_skills_enabled {
|
||||||
@@ -224,11 +263,12 @@ impl HostSkillsService {
|
|||||||
&skill_config_rules,
|
&skill_config_rules,
|
||||||
cache_key,
|
cache_key,
|
||||||
force_reload,
|
force_reload,
|
||||||
|
request_root_snapshots,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
HostSkillsSnapshot::new(Arc::new(
|
HostSkillsSnapshot::new(Arc::new(
|
||||||
self.build_skill_outcome(input, roots, &skill_config_rules)
|
self.build_skill_outcome(input, roots, &skill_config_rules, request_root_snapshots)
|
||||||
.await,
|
.await,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
@@ -249,6 +289,7 @@ impl HostSkillsService {
|
|||||||
skill_config_rules: &SkillConfigRules,
|
skill_config_rules: &SkillConfigRules,
|
||||||
cache_key: ConfigSkillsCacheKey,
|
cache_key: ConfigSkillsCacheKey,
|
||||||
force_reload: bool,
|
force_reload: bool,
|
||||||
|
request_root_snapshots: Option<&RequestSkillRootSnapshots>,
|
||||||
) -> HostSkillsSnapshot {
|
) -> HostSkillsSnapshot {
|
||||||
let snapshot_cell = {
|
let snapshot_cell = {
|
||||||
let mut cache = self
|
let mut cache = self
|
||||||
@@ -271,8 +312,13 @@ impl HostSkillsService {
|
|||||||
snapshot_cell
|
snapshot_cell
|
||||||
.get_or_init(|| async {
|
.get_or_init(|| async {
|
||||||
HostSkillsSnapshot::new(Arc::new(
|
HostSkillsSnapshot::new(Arc::new(
|
||||||
self.build_skill_outcome(input, roots, skill_config_rules)
|
self.build_skill_outcome(
|
||||||
.await,
|
input,
|
||||||
|
roots,
|
||||||
|
skill_config_rules,
|
||||||
|
request_root_snapshots,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -285,12 +331,14 @@ impl HostSkillsService {
|
|||||||
input: &HostSkillsLoadInput,
|
input: &HostSkillsLoadInput,
|
||||||
roots: Vec<HostSkillRoot>,
|
roots: Vec<HostSkillRoot>,
|
||||||
skill_config_rules: &SkillConfigRules,
|
skill_config_rules: &SkillConfigRules,
|
||||||
|
request_root_snapshots: Option<&RequestSkillRootSnapshots>,
|
||||||
) -> SkillLoadOutcome {
|
) -> SkillLoadOutcome {
|
||||||
let outcome = load_and_merge_host_skill_roots(
|
let outcome = load_and_merge_host_skill_roots_with_request_snapshots(
|
||||||
roots,
|
roots,
|
||||||
&self.root_scan_slots,
|
&self.root_scan_slots,
|
||||||
self.restriction_product,
|
self.restriction_product,
|
||||||
input.plugin_skill_snapshots.as_ref(),
|
input.plugin_skill_snapshots.as_ref(),
|
||||||
|
request_root_snapshots,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let disabled_paths = skill_config_rules.resolve_disabled_paths(
|
let disabled_paths = skill_config_rules.resolve_disabled_paths(
|
||||||
@@ -397,7 +445,7 @@ struct ConfigSkillsCacheKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
struct ConfigSkillRootCacheKey {
|
pub(crate) struct ConfigSkillRootCacheKey {
|
||||||
path: AbsolutePathBuf,
|
path: AbsolutePathBuf,
|
||||||
scope_rank: u8,
|
scope_rank: u8,
|
||||||
plugin_identity: Option<PluginIdentity>,
|
plugin_identity: Option<PluginIdentity>,
|
||||||
@@ -450,24 +498,7 @@ fn config_skills_cache_key(
|
|||||||
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
||||||
) -> ConfigSkillsCacheKey {
|
) -> ConfigSkillsCacheKey {
|
||||||
ConfigSkillsCacheKey {
|
ConfigSkillsCacheKey {
|
||||||
roots: roots
|
roots: roots.iter().map(config_skill_root_cache_key).collect(),
|
||||||
.iter()
|
|
||||||
.map(|root| {
|
|
||||||
let scope_rank = match root.scope {
|
|
||||||
SkillScope::Repo => 0,
|
|
||||||
SkillScope::User => 1,
|
|
||||||
SkillScope::System => 2,
|
|
||||||
SkillScope::Admin => 3,
|
|
||||||
};
|
|
||||||
ConfigSkillRootCacheKey {
|
|
||||||
path: root.path.clone(),
|
|
||||||
scope_rank,
|
|
||||||
plugin_identity: root.plugin_identity().cloned(),
|
|
||||||
plugin_namespace: root.plugin_namespace().map(str::to_string),
|
|
||||||
file_system: FileSystemIdentity(Arc::downgrade(&root.file_system)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
skill_config_rules: skill_config_rules.clone(),
|
skill_config_rules: skill_config_rules.clone(),
|
||||||
plugin_skill_snapshots: plugin_skill_snapshots
|
plugin_skill_snapshots: plugin_skill_snapshots
|
||||||
.filter(|_| roots.iter().any(|root| root.plugin_identity().is_some()))
|
.filter(|_| roots.iter().any(|root| root.plugin_identity().is_some()))
|
||||||
@@ -475,6 +506,22 @@ fn config_skills_cache_key(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn config_skill_root_cache_key(root: &HostSkillRoot) -> ConfigSkillRootCacheKey {
|
||||||
|
let scope_rank = match root.scope {
|
||||||
|
SkillScope::Repo => 0,
|
||||||
|
SkillScope::User => 1,
|
||||||
|
SkillScope::System => 2,
|
||||||
|
SkillScope::Admin => 3,
|
||||||
|
};
|
||||||
|
ConfigSkillRootCacheKey {
|
||||||
|
path: root.path.clone(),
|
||||||
|
scope_rank,
|
||||||
|
plugin_identity: root.plugin_identity().cloned(),
|
||||||
|
plugin_namespace: root.plugin_namespace().map(str::to_string),
|
||||||
|
file_system: FileSystemIdentity(Arc::downgrade(&root.file_system)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "host_service_tests.rs"]
|
#[path = "host_service_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -305,6 +305,76 @@ async fn snapshot_for_config_preserves_host_precedence_for_symlinked_plugin_root
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn skills_list_snapshots_share_host_roots_only_within_one_request() {
|
||||||
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let first_cwd = tempfile::tempdir().expect("tempdir");
|
||||||
|
let second_cwd = tempfile::tempdir().expect("tempdir");
|
||||||
|
let config_layer_stack = config_stack(&codex_home, "");
|
||||||
|
let disabled_config_layer_stack = config_stack(
|
||||||
|
&codex_home,
|
||||||
|
&name_toggle_config("first-skill", /*enabled*/ false),
|
||||||
|
);
|
||||||
|
let skills_service = HostSkillsService::new(
|
||||||
|
codex_home.path().abs(),
|
||||||
|
/*bundled_skills_enabled*/ true,
|
||||||
|
);
|
||||||
|
let request = skills_service.for_request();
|
||||||
|
let input = |cwd: &TempDir, config_layer_stack| {
|
||||||
|
HostSkillsLoadInput::new(
|
||||||
|
cwd.path().abs(),
|
||||||
|
Vec::new(),
|
||||||
|
config_layer_stack,
|
||||||
|
/*bundled_skills_enabled*/ true,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
write_user_skill(&codex_home, "first", "first-skill", "first skill");
|
||||||
|
let first = request
|
||||||
|
.snapshot_for_cwd(
|
||||||
|
&input(&first_cwd, config_layer_stack.clone()),
|
||||||
|
/*force_reload*/ false,
|
||||||
|
Some(Arc::clone(&LOCAL_FS)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
write_user_skill(&codex_home, "second", "second-skill", "second skill");
|
||||||
|
let second = request
|
||||||
|
.snapshot_for_cwd(
|
||||||
|
&input(&second_cwd, disabled_config_layer_stack),
|
||||||
|
/*force_reload*/ false,
|
||||||
|
Some(Arc::clone(&LOCAL_FS)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(second.outcome().skills, first.outcome().skills);
|
||||||
|
let first_skill_path = first
|
||||||
|
.outcome()
|
||||||
|
.skills
|
||||||
|
.iter()
|
||||||
|
.find(|skill| skill.name == "first-skill")
|
||||||
|
.expect("first skill should be discovered")
|
||||||
|
.path_to_skills_md
|
||||||
|
.clone();
|
||||||
|
assert!(!first.outcome().disabled_paths.contains(&first_skill_path));
|
||||||
|
assert!(second.outcome().disabled_paths.contains(&first_skill_path));
|
||||||
|
|
||||||
|
let third = skills_service
|
||||||
|
.for_request()
|
||||||
|
.snapshot_for_cwd(
|
||||||
|
&input(&first_cwd, config_layer_stack),
|
||||||
|
/*force_reload*/ true,
|
||||||
|
Some(Arc::clone(&LOCAL_FS)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
third
|
||||||
|
.outcome()
|
||||||
|
.skills
|
||||||
|
.iter()
|
||||||
|
.any(|skill| skill.name == "second-skill")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn skills_for_config_refreshes_cache_when_remote_plugin_id_changes() {
|
async fn skills_for_config_refreshes_cache_when_remote_plugin_id_changes() {
|
||||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||||
@@ -344,6 +414,7 @@ async fn skills_for_config_refreshes_cache_when_remote_plugin_id_changes() {
|
|||||||
.clone()
|
.clone()
|
||||||
.with_plugin_skill_snapshots(/*plugin_skill_snapshots*/ None);
|
.with_plugin_skill_snapshots(/*plugin_skill_snapshots*/ None);
|
||||||
let listing_snapshot = skills_service
|
let listing_snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&listing_input,
|
&listing_input,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
@@ -405,6 +476,7 @@ async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() {
|
|||||||
bundled_skills_enabled_from_stack(&config_layer_stack),
|
bundled_skills_enabled_from_stack(&config_layer_stack),
|
||||||
);
|
);
|
||||||
let empty_snapshot = skills_service
|
let empty_snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&skills_input,
|
&skills_input,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
@@ -430,6 +502,7 @@ async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() {
|
|||||||
skills_service.set_extra_roots(vec![extra_skills_root.abs()]);
|
skills_service.set_extra_roots(vec![extra_skills_root.abs()]);
|
||||||
|
|
||||||
let runtime_snapshot = skills_service
|
let runtime_snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&skills_input,
|
&skills_input,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
@@ -446,6 +519,7 @@ async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() {
|
|||||||
|
|
||||||
skills_service.set_extra_roots(vec![extra_root.path().join("missing-skills").abs()]);
|
skills_service.set_extra_roots(vec![extra_root.path().join("missing-skills").abs()]);
|
||||||
let replaced_snapshot = skills_service
|
let replaced_snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&skills_input,
|
&skills_input,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
@@ -603,6 +677,7 @@ async fn skills_for_cwd_loads_repo_and_user_roots_with_local_fs() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = skills_service
|
let snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&skills_input,
|
&skills_input,
|
||||||
/*force_reload*/ true,
|
/*force_reload*/ true,
|
||||||
@@ -673,6 +748,7 @@ async fn skills_for_cwd_without_fs_skips_repo_roots() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = skills_service
|
let snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(&skills_input, /*force_reload*/ true, /*fs*/ None)
|
.snapshot_for_cwd(&skills_input, /*force_reload*/ true, /*fs*/ None)
|
||||||
.await;
|
.await;
|
||||||
let outcome = snapshot.outcome();
|
let outcome = snapshot.outcome();
|
||||||
@@ -742,9 +818,10 @@ async fn skills_for_cwd_uses_cached_result_until_force_reload() {
|
|||||||
let config_input = base_input
|
let config_input = base_input
|
||||||
.clone()
|
.clone()
|
||||||
.with_plugin_skill_snapshots(Some(test_plugin_skill_snapshots()));
|
.with_plugin_skill_snapshots(Some(test_plugin_skill_snapshots()));
|
||||||
|
let request = skills_service.for_request();
|
||||||
let (config_snapshot, snapshot_a) = tokio::join!(
|
let (config_snapshot, snapshot_a) = tokio::join!(
|
||||||
skills_service.snapshot_for_config(&config_input, Some(Arc::clone(&LOCAL_FS))),
|
skills_service.snapshot_for_config(&config_input, Some(Arc::clone(&LOCAL_FS))),
|
||||||
skills_service.snapshot_for_cwd(
|
request.snapshot_for_cwd(
|
||||||
&base_input,
|
&base_input,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
Some(Arc::clone(&LOCAL_FS)),
|
Some(Arc::clone(&LOCAL_FS)),
|
||||||
@@ -765,6 +842,7 @@ async fn skills_for_cwd_uses_cached_result_until_force_reload() {
|
|||||||
write_user_skill(&codex_home, "late", "late-skill", "added after cache");
|
write_user_skill(&codex_home, "late", "late-skill", "added after cache");
|
||||||
|
|
||||||
let snapshot_b = skills_service
|
let snapshot_b = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&base_input,
|
&base_input,
|
||||||
/*force_reload*/ false,
|
/*force_reload*/ false,
|
||||||
@@ -780,6 +858,7 @@ async fn skills_for_cwd_uses_cached_result_until_force_reload() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let snapshot_reloaded = skills_service
|
let snapshot_reloaded = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&base_input,
|
&base_input,
|
||||||
/*force_reload*/ true,
|
/*force_reload*/ true,
|
||||||
@@ -825,6 +904,7 @@ async fn skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill()
|
|||||||
);
|
);
|
||||||
|
|
||||||
let parent_snapshot = skills_service
|
let parent_snapshot = skills_service
|
||||||
|
.for_request()
|
||||||
.snapshot_for_cwd(
|
.snapshot_for_cwd(
|
||||||
&parent_input,
|
&parent_input,
|
||||||
/*force_reload*/ true,
|
/*force_reload*/ true,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub use host_outcome::SkillLoadOutcome;
|
|||||||
pub use host_prompt::HostSkillPrompts;
|
pub use host_prompt::HostSkillPrompts;
|
||||||
pub use host_prompt::InjectedHostSkillPrompts;
|
pub use host_prompt::InjectedHostSkillPrompts;
|
||||||
pub use host_service::HostSkillsLoadInput;
|
pub use host_service::HostSkillsLoadInput;
|
||||||
|
pub use host_service::HostSkillsRequest;
|
||||||
pub use host_service::HostSkillsService;
|
pub use host_service::HostSkillsService;
|
||||||
pub use host_service::bundled_skills_enabled_from_stack;
|
pub use host_service::bundled_skills_enabled_from_stack;
|
||||||
pub use host_snapshot::HostSkillsSnapshot;
|
pub use host_snapshot::HostSkillsSnapshot;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
|||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use codex_utils_plugins::PluginSkillRoot;
|
use codex_utils_plugins::PluginSkillRoot;
|
||||||
use codex_utils_plugins::migrated_command_skills_root;
|
use codex_utils_plugins::migrated_command_skills_root;
|
||||||
|
use futures::FutureExt;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
@@ -20,12 +21,31 @@ use super::MAX_CONCURRENT_ROOT_SCANS;
|
|||||||
use super::host::HostSkillRootSnapshot;
|
use super::host::HostSkillRootSnapshot;
|
||||||
use super::load_host_skill_root;
|
use super::load_host_skill_root;
|
||||||
use crate::SkillLoadOutcome;
|
use crate::SkillLoadOutcome;
|
||||||
|
use crate::host_service::RequestSkillRootSnapshots;
|
||||||
|
use crate::host_service::config_skill_root_cache_key;
|
||||||
|
|
||||||
pub(crate) async fn load_and_merge_host_skill_roots(
|
pub(crate) async fn load_and_merge_host_skill_roots(
|
||||||
roots: Vec<HostSkillRoot>,
|
roots: Vec<HostSkillRoot>,
|
||||||
root_scan_slots: &Semaphore,
|
root_scan_slots: &Semaphore,
|
||||||
restriction_product: Option<Product>,
|
restriction_product: Option<Product>,
|
||||||
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
||||||
|
) -> SkillLoadOutcome {
|
||||||
|
load_and_merge_host_skill_roots_with_request_snapshots(
|
||||||
|
roots,
|
||||||
|
root_scan_slots,
|
||||||
|
restriction_product,
|
||||||
|
plugin_skill_snapshots,
|
||||||
|
/*request_root_snapshots*/ None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_and_merge_host_skill_roots_with_request_snapshots(
|
||||||
|
roots: Vec<HostSkillRoot>,
|
||||||
|
root_scan_slots: &Semaphore,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
|
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
||||||
|
request_root_snapshots: Option<&RequestSkillRootSnapshots>,
|
||||||
) -> SkillLoadOutcome {
|
) -> SkillLoadOutcome {
|
||||||
let migrated_command_roots_by_plugin = roots
|
let migrated_command_roots_by_plugin = roots
|
||||||
.iter()
|
.iter()
|
||||||
@@ -42,42 +62,63 @@ pub(crate) async fn load_and_merge_host_skill_roots(
|
|||||||
.cloned();
|
.cloned();
|
||||||
let plugin_skill_root = root.plugin_skill_root();
|
let plugin_skill_root = root.plugin_skill_root();
|
||||||
let file_system = Arc::clone(&root.file_system);
|
let file_system = Arc::clone(&root.file_system);
|
||||||
let _root_scan_slot = root_scan_slots
|
let request_snapshot = if plugin_skill_root.is_none() {
|
||||||
.acquire()
|
request_root_snapshots.map(|snapshots| {
|
||||||
.await
|
Arc::clone(
|
||||||
.unwrap_or_else(|_| unreachable!());
|
snapshots
|
||||||
let cached_snapshot = plugin_skill_root
|
.write()
|
||||||
.as_ref()
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
.and_then(|plugin_skill_root| plugin_skill_snapshots?.get(plugin_skill_root));
|
.entry(config_skill_root_cache_key(&root))
|
||||||
let snapshot = match cached_snapshot {
|
.or_insert_with(|| Arc::new(OnceCell::new())),
|
||||||
Some(snapshot) => HostSkillRootSnapshot {
|
)
|
||||||
root: snapshot.root,
|
})
|
||||||
skills: snapshot.skills,
|
} else {
|
||||||
skill_discovery_path_by_path: snapshot.skill_discovery_path_by_path,
|
None
|
||||||
errors: snapshot.errors,
|
};
|
||||||
file_system: Arc::clone(&file_system),
|
let load = async {
|
||||||
is_agent_plugin: snapshot.is_agent_plugin,
|
let _root_scan_slot = root_scan_slots
|
||||||
},
|
.acquire()
|
||||||
None => {
|
.await
|
||||||
let snapshot = load_host_skill_root(root).await;
|
.unwrap_or_else(|_| unreachable!());
|
||||||
if let Some(plugin_skill_snapshots) = plugin_skill_snapshots
|
let cached_snapshot = plugin_skill_root
|
||||||
&& let Some(plugin_skill_root) = plugin_skill_root
|
.as_ref()
|
||||||
{
|
.and_then(|plugin_skill_root| plugin_skill_snapshots?.get(plugin_skill_root));
|
||||||
plugin_skill_snapshots.insert(
|
match cached_snapshot {
|
||||||
plugin_skill_root,
|
Some(snapshot) => HostSkillRootSnapshot {
|
||||||
LoadedSkillRoot {
|
root: snapshot.root,
|
||||||
root: snapshot.root.clone(),
|
skills: snapshot.skills,
|
||||||
skills: snapshot.skills.clone(),
|
skill_discovery_path_by_path: snapshot.skill_discovery_path_by_path,
|
||||||
skill_discovery_path_by_path: Arc::clone(
|
errors: snapshot.errors,
|
||||||
&snapshot.skill_discovery_path_by_path,
|
file_system: Arc::clone(&file_system),
|
||||||
),
|
is_agent_plugin: snapshot.is_agent_plugin,
|
||||||
errors: snapshot.errors.clone(),
|
},
|
||||||
is_agent_plugin: snapshot.is_agent_plugin,
|
None => {
|
||||||
},
|
let snapshot = load_host_skill_root(root).await;
|
||||||
);
|
if let Some(plugin_skill_snapshots) = plugin_skill_snapshots
|
||||||
|
&& let Some(plugin_skill_root) = plugin_skill_root
|
||||||
|
{
|
||||||
|
plugin_skill_snapshots.insert(
|
||||||
|
plugin_skill_root,
|
||||||
|
LoadedSkillRoot {
|
||||||
|
root: snapshot.root.clone(),
|
||||||
|
skills: snapshot.skills.clone(),
|
||||||
|
skill_discovery_path_by_path: Arc::clone(
|
||||||
|
&snapshot.skill_discovery_path_by_path,
|
||||||
|
),
|
||||||
|
errors: snapshot.errors.clone(),
|
||||||
|
is_agent_plugin: snapshot.is_agent_plugin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
snapshot
|
||||||
}
|
}
|
||||||
snapshot
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.boxed();
|
||||||
|
let snapshot = if let Some(snapshot) = request_snapshot {
|
||||||
|
snapshot.get_or_init(|| load).await.clone()
|
||||||
|
} else {
|
||||||
|
load.await
|
||||||
};
|
};
|
||||||
let migrated_command_root = match (plugin_root.as_ref(), migrated_command_root) {
|
let migrated_command_root = match (plugin_root.as_ref(), migrated_command_root) {
|
||||||
(Some(plugin_root), Some(migrated_command_root)) => Some(
|
(Some(plugin_root), Some(migrated_command_root)) => Some(
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ mod namespace;
|
|||||||
pub(crate) use environment::load_environment_skills_from_discovery;
|
pub(crate) use environment::load_environment_skills_from_discovery;
|
||||||
pub(crate) use environment::load_environment_skills_from_root;
|
pub(crate) use environment::load_environment_skills_from_root;
|
||||||
pub use host::HostSkillRoot;
|
pub use host::HostSkillRoot;
|
||||||
|
pub(crate) use host::HostSkillRootSnapshot;
|
||||||
pub(crate) use host::load_host_skill_root;
|
pub(crate) use host::load_host_skill_root;
|
||||||
pub(crate) use host_merge::load_and_merge_host_skill_roots;
|
pub(crate) use host_merge::load_and_merge_host_skill_roots;
|
||||||
|
pub(crate) use host_merge::load_and_merge_host_skill_roots_with_request_snapshots;
|
||||||
|
|
||||||
pub(crate) const MAX_CONCURRENT_ROOT_SCANS: usize = 8;
|
pub(crate) const MAX_CONCURRENT_ROOT_SCANS: usize = 8;
|
||||||
pub(super) const SKILLS_FILENAME: &str = "SKILL.md";
|
pub(super) const SKILLS_FILENAME: &str = "SKILL.md";
|
||||||
|
|||||||
Reference in new issue
Block a user