vtcode-core 0.175.6

Core library for VT Code - a Rust-based terminal coding agent
Documentation
use std::path::{Path, PathBuf};
use std::sync::{OnceLock, RwLock};
use std::time::{Duration, SystemTime};

use hashbrown::HashMap;
use tracing::warn;

use super::SkillLoaderConfig;
use crate::skills::model::SkillLoadOutcome;

const LIGHTWEIGHT_SKILL_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
const LIGHTWEIGHT_SKILL_CACHE_MAX_ENTRIES: usize = 32;

static LIGHTWEIGHT_SKILL_METADATA_CACHE: OnceLock<
    RwLock<HashMap<LightweightSkillCacheKey, CachedLightweightSkillOutcome>>,
> = OnceLock::new();

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct LightweightSkillCacheKey {
    codex_home: PathBuf,
    cwd: PathBuf,
    project_root: Option<PathBuf>,
    include_bundled_system_skills: bool,
    home_dir: Option<PathBuf>,
}

impl LightweightSkillCacheKey {
    pub(crate) fn new(config: &SkillLoaderConfig, home_dir: Option<&Path>) -> Self {
        Self {
            codex_home: normalize_cache_path(&config.codex_home),
            cwd: normalize_cache_path(&config.cwd),
            project_root: config.project_root.as_deref().map(normalize_cache_path),
            include_bundled_system_skills: config.include_bundled_system_skills,
            home_dir: home_dir.map(normalize_cache_path),
        }
    }
}

#[derive(Clone)]
struct CachedLightweightSkillOutcome {
    outcome: SkillLoadOutcome,
    timestamp: SystemTime,
}

impl CachedLightweightSkillOutcome {
    fn is_expired(&self) -> bool {
        self.timestamp.elapsed().unwrap_or(LIGHTWEIGHT_SKILL_CACHE_TTL) > LIGHTWEIGHT_SKILL_CACHE_TTL
    }
}

fn normalize_cache_path(path: &Path) -> PathBuf {
    dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}

fn lightweight_skill_metadata_cache()
-> &'static RwLock<HashMap<LightweightSkillCacheKey, CachedLightweightSkillOutcome>> {
    LIGHTWEIGHT_SKILL_METADATA_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
}

pub(crate) fn get_cached_lightweight_skill_outcome(key: &LightweightSkillCacheKey) -> Option<SkillLoadOutcome> {
    match lightweight_skill_metadata_cache().read() {
        Ok(cache) => cache
            .get(key)
            .filter(|cached| !cached.is_expired())
            .map(|cached| cached.outcome.clone()),
        Err(_) => {
            warn!("lightweight skill metadata cache lock poisoned while reading cache");
            None
        }
    }
}

pub(crate) fn cache_lightweight_skill_outcome(key: LightweightSkillCacheKey, outcome: &SkillLoadOutcome) {
    match lightweight_skill_metadata_cache().write() {
        Ok(mut cache) => {
            if cache.len() >= LIGHTWEIGHT_SKILL_CACHE_MAX_ENTRIES && !cache.contains_key(&key) {
                let expired: Vec<_> = cache
                    .iter()
                    .filter(|(_, value)| value.is_expired())
                    .map(|(cache_key, _)| cache_key.clone())
                    .collect();

                for cache_key in expired {
                    cache.remove(&cache_key);
                }

                if cache.len() >= LIGHTWEIGHT_SKILL_CACHE_MAX_ENTRIES {
                    let oldest_key = cache
                        .iter()
                        .min_by_key(|(_, value)| value.timestamp)
                        .map(|(cache_key, _)| cache_key.clone());
                    if let Some(oldest_key) = oldest_key {
                        cache.remove(&oldest_key);
                    }
                }
            }

            cache.insert(
                key,
                CachedLightweightSkillOutcome {
                    outcome: outcome.clone(),
                    timestamp: SystemTime::now(),
                },
            );
        }
        Err(_) => warn!("lightweight skill metadata cache lock poisoned while writing cache"),
    }
}

pub(crate) fn clear_lightweight_skill_metadata_cache() {
    match lightweight_skill_metadata_cache().write() {
        Ok(mut cache) => cache.clear(),
        Err(_) => warn!("lightweight skill metadata cache lock poisoned while clearing cache"),
    }
}