Skip to main content

vtcode_core/utils/
session_debug.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::{LazyLock, Mutex};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use anyhow::{Context, Result};
7
8use crate::core::SECONDS_PER_DAY;
9use crate::utils::dot_config::DotManager;
10use crate::utils::session_archive::SESSION_DIR_ENV;
11
12#[derive(Debug, Clone, Default)]
13struct RuntimeDebugContext {
14    debug_session_id: Option<String>,
15    archive_session_id: Option<String>,
16    debug_log_path: Option<PathBuf>,
17}
18
19static RUNTIME_DEBUG_CONTEXT: LazyLock<Mutex<RuntimeDebugContext>> =
20    LazyLock::new(|| Mutex::new(RuntimeDebugContext::default()));
21
22pub const DEFAULT_MAX_DEBUG_LOG_SIZE_MB: u64 = 50;
23pub const DEFAULT_MAX_DEBUG_LOG_AGE_DAYS: u32 = 7;
24
25const DEBUG_LOG_FILE_PREFIX: &str = "debug-";
26const DEBUG_BYTES_PER_MB: u64 = 1024 * 1024;
27
28fn with_runtime_debug_context<R>(f: impl FnOnce(&mut RuntimeDebugContext) -> R) -> R {
29    match RUNTIME_DEBUG_CONTEXT.lock() {
30        Ok(mut context) => f(&mut context),
31        Err(poisoned) => {
32            let mut context = poisoned.into_inner();
33            f(&mut context)
34        }
35    }
36}
37
38pub fn configure_runtime_debug_context(
39    debug_session_id: String,
40    archive_session_id: Option<String>,
41) {
42    with_runtime_debug_context(|context| {
43        context.debug_session_id = Some(debug_session_id);
44        context.archive_session_id = archive_session_id;
45        context.debug_log_path = None;
46    });
47}
48
49pub fn set_runtime_archive_session_id(archive_session_id: Option<String>) {
50    with_runtime_debug_context(|context| {
51        context.archive_session_id = archive_session_id;
52    });
53}
54
55pub fn runtime_archive_session_id() -> Option<String> {
56    with_runtime_debug_context(|context| context.archive_session_id.clone())
57}
58
59pub fn runtime_debug_log_path() -> Option<PathBuf> {
60    with_runtime_debug_context(|context| context.debug_log_path.clone())
61}
62
63pub fn set_runtime_debug_log_path(path: impl Into<PathBuf>) {
64    with_runtime_debug_context(|context| {
65        context.debug_log_path = Some(path.into());
66    });
67}
68
69pub fn sanitize_debug_component(value: &str, fallback: &str) -> String {
70    let mut normalized = String::new();
71    let mut last_was_separator = false;
72    for ch in value.chars() {
73        if ch.is_ascii_alphanumeric() {
74            normalized.push(ch.to_ascii_lowercase());
75            last_was_separator = false;
76        } else if matches!(ch, '-' | '_') {
77            if !last_was_separator {
78                normalized.push(ch);
79                last_was_separator = true;
80            }
81        } else if !last_was_separator {
82            normalized.push('-');
83            last_was_separator = true;
84        }
85    }
86
87    let trimmed = normalized.trim_matches(|c| c == '-' || c == '_');
88    if trimmed.is_empty() {
89        fallback.to_string()
90    } else {
91        trimmed.to_string()
92    }
93}
94
95pub fn build_command_debug_session_id(mode_hint: &str) -> String {
96    let mode = sanitize_debug_component(mode_hint, "cmd");
97    let timestamp = SystemTime::now()
98        .duration_since(UNIX_EPOCH)
99        .unwrap_or_default()
100        .as_millis();
101    format!("cmd-{mode}-{timestamp}-{}", std::process::id())
102}
103
104pub fn current_debug_session_id() -> String {
105    with_runtime_debug_context(|context| context.debug_session_id.clone())
106        .unwrap_or_else(|| build_command_debug_session_id("default"))
107}
108
109fn debug_log_file_name(session_id: &str) -> String {
110    let normalized = sanitize_debug_component(session_id, "session");
111    format!("{DEBUG_LOG_FILE_PREFIX}{normalized}.log")
112}
113
114pub fn default_sessions_dir() -> PathBuf {
115    if let Some(custom) = std::env::var_os(SESSION_DIR_ENV) {
116        return PathBuf::from(custom);
117    }
118    if let Ok(manager) = DotManager::new() {
119        return manager.sessions_dir();
120    }
121    PathBuf::from(".vtcode/sessions")
122}
123
124fn is_debug_log_file(path: &Path) -> bool {
125    let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
126        return false;
127    };
128    name.starts_with(DEBUG_LOG_FILE_PREFIX) && name.ends_with(".log")
129}
130
131fn prune_expired_debug_logs(log_dir: &Path, max_age_days: u32) -> Result<()> {
132    let cutoff = if max_age_days == 0 {
133        SystemTime::now()
134    } else {
135        SystemTime::now()
136            .checked_sub(Duration::from_secs(
137                u64::from(max_age_days).saturating_mul(SECONDS_PER_DAY),
138            ))
139            .unwrap_or(UNIX_EPOCH)
140    };
141
142    for entry in fs::read_dir(log_dir)
143        .with_context(|| format!("Failed to read debug log directory {}", log_dir.display()))?
144    {
145        let entry = match entry {
146            Ok(value) => value,
147            Err(err) => {
148                tracing::warn!(
149                    log_dir = %log_dir.display(),
150                    error = %err,
151                    "Failed to read a debug log entry"
152                );
153                continue;
154            }
155        };
156        let path = entry.path();
157        if !is_debug_log_file(&path) {
158            continue;
159        }
160        let metadata = match entry.metadata() {
161            Ok(value) => value,
162            Err(err) => {
163                tracing::warn!(
164                    path = %path.display(),
165                    error = %err,
166                    "Failed to read debug log metadata"
167                );
168                continue;
169            }
170        };
171        if !metadata.is_file() {
172            continue;
173        }
174        if metadata.modified().unwrap_or(UNIX_EPOCH) <= cutoff
175            && let Err(err) = fs::remove_file(&path)
176        {
177            tracing::warn!(
178                path = %path.display(),
179                error = %err,
180                "Failed to remove expired debug log"
181            );
182        }
183    }
184
185    Ok(())
186}
187
188fn rotate_debug_log_if_needed(log_file: &Path, session_id: &str, max_size_mb: u64) -> Result<()> {
189    if max_size_mb == 0 {
190        return Ok(());
191    }
192
193    let max_bytes = max_size_mb.saturating_mul(DEBUG_BYTES_PER_MB);
194    let metadata = match fs::metadata(log_file) {
195        Ok(value) => value,
196        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
197        Err(err) => {
198            return Err(err)
199                .with_context(|| format!("Failed to inspect debug log {}", log_file.display()));
200        }
201    };
202
203    if metadata.len() < max_bytes {
204        return Ok(());
205    }
206
207    let timestamp = SystemTime::now()
208        .duration_since(UNIX_EPOCH)
209        .unwrap_or_default()
210        .as_millis();
211    let normalized_session_id = sanitize_debug_component(session_id, "session");
212    let rotated_name = format!(
213        "{DEBUG_LOG_FILE_PREFIX}{normalized_session_id}-rotated-{}-{}.log",
214        timestamp,
215        std::process::id()
216    );
217    let rotated_path = log_file
218        .parent()
219        .unwrap_or_else(|| Path::new("."))
220        .join(rotated_name);
221
222    fs::rename(log_file, &rotated_path).with_context(|| {
223        format!(
224            "Failed to rotate debug log {} -> {}",
225            log_file.display(),
226            rotated_path.display()
227        )
228    })?;
229    Ok(())
230}
231
232pub fn prepare_debug_log_file(
233    configured_dir: Option<PathBuf>,
234    session_id: &str,
235    max_size_mb: u64,
236    max_age_days: u32,
237) -> Result<PathBuf> {
238    let log_dir = configured_dir.unwrap_or_else(default_sessions_dir);
239    fs::create_dir_all(&log_dir)
240        .with_context(|| format!("Failed to create debug log directory {}", log_dir.display()))?;
241    prune_expired_debug_logs(&log_dir, max_age_days)?;
242    let log_file = log_dir.join(debug_log_file_name(session_id));
243    rotate_debug_log_if_needed(&log_file, session_id, max_size_mb)?;
244    Ok(log_file)
245}