Skip to main content

algocline_app/service/
mod.rs

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