prodex 0.43.0

OpenAI profile pooling and safe auto-rotate for Codex CLI and Claude Code
Documentation
use super::*;

pub(super) fn is_history_jsonl(path: &Path) -> bool {
    path.file_name()
        .and_then(|name| name.to_str())
        .is_some_and(|name| name == "history.jsonl")
}

pub(super) fn merge_history_files(source: &Path, destination: &Path) -> Result<()> {
    #[derive(Debug)]
    struct HistoryLine {
        ts: Option<i64>,
        line: String,
        order: usize,
    }

    fn load_history_lines(
        path: &Path,
        merged: &mut Vec<HistoryLine>,
        seen: &mut BTreeSet<String>,
    ) -> Result<()> {
        let content = fs::read_to_string(path)
            .with_context(|| format!("failed to read {}", path.display()))?;
        for raw_line in content.lines() {
            let line = raw_line.trim_end_matches('\r');
            if line.is_empty() || !seen.insert(line.to_string()) {
                continue;
            }

            let ts = serde_json::from_str::<serde_json::Value>(line)
                .ok()
                .and_then(|value| value.get("ts").and_then(serde_json::Value::as_i64));
            merged.push(HistoryLine {
                ts,
                line: line.to_string(),
                order: merged.len(),
            });
        }

        Ok(())
    }

    let mut merged = Vec::new();
    let mut seen = BTreeSet::new();

    if destination.exists() {
        load_history_lines(destination, &mut merged, &mut seen)?;
    }
    load_history_lines(source, &mut merged, &mut seen)?;

    merged.sort_by(|left, right| match (left.ts, right.ts) {
        (Some(left_ts), Some(right_ts)) => {
            left_ts.cmp(&right_ts).then(left.order.cmp(&right.order))
        }
        _ => left.order.cmp(&right.order),
    });

    let mut content = String::new();
    for (index, entry) in merged.iter().enumerate() {
        if index > 0 {
            content.push('\n');
        }
        content.push_str(&entry.line);
    }

    fs::write(destination, content)
        .with_context(|| format!("failed to write merged history {}", destination.display()))
}