Skip to main content

algocline_app/service/
mod.rs

1pub(crate) mod alc_toml;
2mod config;
3mod engine_api_impl;
4mod eval;
5mod eval_store;
6mod init;
7pub(crate) mod lockfile;
8mod logging;
9pub(crate) mod manifest;
10mod migrate;
11pub(crate) mod path;
12mod pkg;
13mod pkg_link;
14mod pkg_unlink;
15pub(crate) mod project;
16pub mod resolve;
17mod run;
18mod scenario;
19pub(crate) mod source;
20mod status;
21mod transcript;
22mod update;
23
24#[cfg(test)]
25mod test_support;
26#[cfg(test)]
27mod tests;
28
29use std::path::Path;
30use std::sync::Arc;
31
32use algocline_engine::{Executor, SessionRegistry};
33
34pub use algocline_core::{EngineApi, TokenUsage};
35pub use config::{AppConfig, LogDirSource};
36pub use resolve::{QueryResponse, SearchPath};
37
38// ─── Application Service ────────────────────────────────────────
39
40/// Tracks which sessions are eval sessions and their strategy name.
41///
42/// `std::sync::Mutex` is used (not tokio) because all operations are
43/// single HashMap insert/remove/get completing in microseconds, and no
44/// `.await` is held across the lock. Called from async context but never
45/// held across yield points. Poison is silently skipped — eval tracking
46/// is non-critical metadata.
47type EvalSessions = std::sync::Mutex<std::collections::HashMap<String, String>>;
48
49/// Tracks session_id → strategy name for all strategy-based sessions (advice, eval).
50///
51/// Same locking rationale as `EvalSessions`. Used by `alc_status` and
52/// transcript logging. Poison is silently skipped — strategy name is
53/// non-critical metadata for observability.
54type SessionStrategies = std::sync::Mutex<std::collections::HashMap<String, String>>;
55
56#[derive(Clone)]
57pub struct AppService {
58    executor: Arc<Executor>,
59    registry: Arc<SessionRegistry>,
60    log_config: AppConfig,
61    /// Package search paths in priority order (first = highest).
62    search_paths: Vec<resolve::SearchPath>,
63    /// session_id → strategy name for eval sessions (cleared on completion).
64    eval_sessions: Arc<EvalSessions>,
65    /// session_id → strategy name for log/stats tracking (cleared on session completion).
66    session_strategies: Arc<SessionStrategies>,
67}
68
69impl AppService {
70    pub fn new(
71        executor: Arc<Executor>,
72        log_config: AppConfig,
73        search_paths: Vec<resolve::SearchPath>,
74    ) -> Self {
75        let registry = Arc::new(SessionRegistry::new());
76        // TTL = 3 hours. Complex strategies may run 30–60 min; 3h covers
77        // legitimate paused sessions while eventually reclaiming abandoned ones.
78        registry.spawn_gc_task(std::time::Duration::from_secs(10800));
79        Self {
80            executor,
81            registry,
82            log_config,
83            search_paths,
84            eval_sessions: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
85            session_strategies: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
86        }
87    }
88
89    /// Returns the log directory, or an error if file logging is unavailable.
90    fn require_log_dir(&self) -> Result<&Path, String> {
91        self.log_config
92            .log_dir
93            .as_deref()
94            .ok_or_else(|| "File logging is not available (no writable log directory)".to_string())
95    }
96
97    /// Resolve extra lib paths for a request.
98    ///
99    /// 1. Determines the project root from `project_root` (explicit) or
100    ///    `ALC_PROJECT_ROOT` env or ancestor walk from cwd.
101    /// 2. Reads `alc.lock` from that root.
102    /// 3. Returns the resolved absolute paths of all `local_dir` entries.
103    ///
104    /// Returns an empty `Vec` if no project root is found, if `alc.lock`
105    /// does not exist, or if no `local_dir` entries are present.
106    pub(crate) fn resolve_extra_lib_paths(
107        &self,
108        project_root: Option<&str>,
109    ) -> Vec<std::path::PathBuf> {
110        let Some(root) = project::resolve_project_root(project_root) else {
111            return vec![];
112        };
113
114        match lockfile::load_lockfile(&root) {
115            Ok(Some(lock)) => {
116                self.warn_toml_lock_mismatch(&root, &lock);
117                lockfile::resolve_path_entries(&root, &lock)
118            }
119            Ok(None) => vec![],
120            Err(e) => {
121                tracing::warn!("failed to load alc.lock at {}: {e}", root.display());
122                vec![]
123            }
124        }
125    }
126
127    fn warn_toml_lock_mismatch(&self, root: &Path, lock: &lockfile::LockFile) {
128        let toml = match alc_toml::load_alc_toml(root) {
129            Ok(Some(t)) => t,
130            _ => return,
131        };
132
133        use std::collections::BTreeSet;
134        let toml_names: BTreeSet<&str> = toml.packages.keys().map(|s| s.as_str()).collect();
135        let lock_names: BTreeSet<&str> = lock.packages.iter().map(|p| p.name.as_str()).collect();
136
137        for name in toml_names.difference(&lock_names) {
138            eprintln!(
139                "warning: '{name}' is declared in alc.toml but missing from alc.lock. Run `alc_update` to sync."
140            );
141        }
142        for name in lock_names.difference(&toml_names) {
143            eprintln!("warning: '{name}' is in alc.lock but not declared in alc.toml.");
144        }
145    }
146}