use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref HISTORY_MUTEXES: Mutex<HashMap<String, Mutex<()>>> = Mutex::new(HashMap::new());
}
pub fn get_history_dir() -> Result<PathBuf> {
let data_dir = crate::directories::get_octomind_data_dir()?;
let history_dir = data_dir.join("history");
let legacy_history_file = data_dir.join("history");
if legacy_history_file.exists() && legacy_history_file.is_file() {
let backup_path = data_dir.join("history.legacy");
fs::rename(&legacy_history_file, &backup_path).with_context(|| {
format!(
"Failed to rename legacy history file from {:?} to {:?}",
legacy_history_file, backup_path
)
})?;
crate::log_debug!(
"Renamed legacy history file to: {:?} (will be migrated on first use)",
backup_path
);
}
if !history_dir.exists() {
fs::create_dir_all(&history_dir)
.with_context(|| format!("Failed to create history directory: {:?}", history_dir))?;
}
Ok(history_dir)
}
pub fn get_session_history_file_path(role: &str) -> Result<PathBuf> {
let history_dir = get_history_dir()?;
Ok(history_dir.join(format!("session_{}.history", role)))
}
pub fn get_ask_history_file_path() -> Result<PathBuf> {
let history_dir = get_history_dir()?;
Ok(history_dir.join("ask.history"))
}
fn get_role_mutex(role: &str) -> std::sync::MutexGuard<'static, ()> {
let mut mutexes = HISTORY_MUTEXES.lock().unwrap();
if !mutexes.contains_key(role) {
mutexes.insert(role.to_string(), Mutex::new(()));
}
let mutex_ref = mutexes.get(role).unwrap() as *const Mutex<()>;
drop(mutexes);
unsafe { (*mutex_ref).lock().unwrap() }
}
fn migrate_legacy_history_if_needed(role: &str) -> Result<()> {
if role != "developer" {
return Ok(());
}
let data_dir = crate::directories::get_octomind_data_dir()?;
let legacy_backup = data_dir.join("history.legacy");
let role_history_path = get_session_history_file_path(role)?;
if legacy_backup.exists() && legacy_backup.is_file() && !role_history_path.exists() {
crate::log_debug!(
"Migrating legacy global history to role-based system for role: {}",
role
);
let legacy_content = fs::read_to_string(&legacy_backup)
.with_context(|| format!("Failed to read legacy history file: {:?}", legacy_backup))?;
let mut role_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&role_history_path)
.with_context(|| {
format!(
"Failed to create role history file: {:?}",
role_history_path
)
})?;
writeln!(role_file, "# OCTOMIND_HISTORY_VERSION=1")?;
for line in legacy_content.lines() {
if !line.trim().is_empty() {
let encoded_line = line.replace("\\", "\\\\").replace("\n", "\\n");
writeln!(role_file, "{}", encoded_line)?;
}
}
role_file.flush()?;
let migrated_path = data_dir.join("history.migrated");
fs::rename(&legacy_backup, &migrated_path).with_context(|| {
format!(
"Failed to rename legacy backup file to: {:?}",
migrated_path
)
})?;
crate::log_debug!(
"Successfully migrated legacy history to: {:?}",
role_history_path
);
crate::log_debug!("Legacy file marked as migrated: {:?}", migrated_path);
}
Ok(())
}
pub fn load_session_history_from_file(role: &str) -> Result<Vec<String>> {
let _lock = get_role_mutex(role);
migrate_legacy_history_if_needed(role)?;
let history_path = get_session_history_file_path(role)?;
if !history_path.exists() {
return Ok(Vec::new());
}
let file = std::fs::File::open(&history_path)
.with_context(|| format!("Failed to open history file: {:?}", history_path))?;
let reader = BufReader::new(file);
let mut history_lines = Vec::new();
for line in reader.lines() {
let line = line.with_context(|| "Failed to read line from history file")?;
if line.starts_with("# OCTOMIND_HISTORY_VERSION") || line.trim().is_empty() {
continue;
}
let decoded_line = line.replace("\\n", "\n").replace("\\\\", "\\");
history_lines.push(decoded_line);
}
Ok(history_lines)
}
pub fn append_to_session_history_file(role: &str, line: &str) -> Result<()> {
let _lock = get_role_mutex(role);
let history_path = get_session_history_file_path(role)?;
if !history_path.exists() {
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&history_path)
.with_context(|| format!("Failed to create history file: {:?}", history_path))?;
writeln!(file, "# OCTOMIND_HISTORY_VERSION=1")?;
file.flush()?;
}
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&history_path)
.with_context(|| format!("Failed to open history file for append: {:?}", history_path))?;
let encoded_line = line.replace("\\", "\\\\").replace("\n", "\\n");
writeln!(file, "{}", encoded_line)?;
file.flush()?;
Ok(())
}