//! Parsing and export helpers for external-agent session histories.mod detect;mod export;mod ledger;mod records;use codex_protocol::protocol::RolloutItem;use std::io;use std::path::Path;use std::path::PathBuf;pub use detect::detect_recent_sessions;use export::load_session_for_import_with_content_sha256;pub use ledger::CompletedExternalAgentSessionImport;pub use ledger::has_current_session_been_imported;pub use ledger::record_completed_session_imports;pub use records::SessionSummary;pub use records::summarize_session;const SESSION_TITLE_MAX_LEN: usize = 120;#[derive(Debug, Clone, PartialEq, Eq)]pub struct ExternalAgentSessionMigration { pub path: PathBuf, pub cwd: PathBuf, pub title: Option<String>,}#[derive(Debug, Clone)]pub struct ImportedExternalAgentSession { pub cwd: PathBuf, pub title: Option<String>, pub first_user_message: Option<String>, pub rollout_items: Vec<RolloutItem>,}#[derive(Debug, Clone)]pub struct PendingSessionImport { pub source_path: PathBuf, pub source_content_sha256: String, pub session: ImportedExternalAgentSession,}pub fn prepare_validated_session_import( codex_home: &Path, session: ExternalAgentSessionMigration,) -> io::Result<Option<PendingSessionImport>> { let has_been_imported = has_current_session_been_imported(codex_home, &session.path)?; if has_been_imported { return Ok(None); } let Some((source_path, imported_session, source_content_sha256)) = load_importable_session(&session.path)? else { return Ok(None); }; Ok(Some(PendingSessionImport { source_path, source_content_sha256, session: imported_session, }))}fn load_importable_session( path: &Path,) -> io::Result<Option<(PathBuf, ImportedExternalAgentSession, String)>> { let source_path = std::fs::canonicalize(path)?; let Some((imported_session, source_content_sha256)) = load_session_for_import_with_content_sha256(&source_path)? else { return Ok(None); }; Ok(imported_session.cwd.is_dir().then_some(( source_path, imported_session, source_content_sha256, )))}#[derive(Debug, Clone)]struct ConversationMessage { role: MessageRole, text: String, timestamp: Option<i64>,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum MessageRole { Assistant, User,}fn summarize_for_label(text: &str) -> String { let first_line = text.lines().next().unwrap_or_default().trim(); truncate(first_line, SESSION_TITLE_MAX_LEN)}fn truncate(text: &str, max_len: usize) -> String { if text.chars().count() <= max_len { return text.to_string(); } let prefix = text .chars() .take(max_len.saturating_sub(3)) .collect::<String>(); format!("{prefix}...")}fn now_unix_seconds() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|duration| duration.as_secs() as i64) .unwrap_or_default()}#[cfg(test)]mod tests { use super::*; use codex_protocol::ThreadId; use sha2::Digest; use sha2::Sha256; use tempfile::TempDir; #[test] fn skips_session_that_was_already_imported() { let root = TempDir::new().expect("tempdir"); let codex_home = root.path().join("codex-home"); let source_path = root.path().join("session.jsonl"); std::fs::write(&source_path, "{}\n").expect("session"); ledger::record_imported_session(&codex_home, &source_path, ThreadId::new()) .expect("record import"); let pending = prepare_validated_session_import(&codex_home, session_migration(&source_path)) .expect("already imported session should be skipped"); assert!(pending.is_none()); } #[test] fn reports_session_preparation_errors() { let root = TempDir::new().expect("tempdir"); let source_path = root.path().join("missing-session.jsonl"); let err = prepare_validated_session_import(root.path(), session_migration(&source_path)) .expect_err("missing session should fail preparation"); assert_eq!(err.kind(), io::ErrorKind::NotFound); } #[test] fn prepares_one_validated_session_import_with_content_hash() { let root = TempDir::new().expect("tempdir"); let source_path = root.path().join("session.jsonl"); let contents = serde_json::json!({ "type": "user", "cwd": root.path(), "timestamp": "2026-06-03T12:00:00Z", "message": { "content": "first request" }, }) .to_string(); std::fs::write(&source_path, &contents).expect("session"); let pending = prepare_validated_session_import(root.path(), session_migration(&source_path)) .expect("prepare session") .expect("pending import"); assert_eq!( pending.source_content_sha256, format!("{:x}", Sha256::digest(contents)) ); } fn session_migration(path: &Path) -> ExternalAgentSessionMigration { ExternalAgentSessionMigration { path: path.to_path_buf(), cwd: path .parent() .expect("source path should have parent") .to_path_buf(), title: None, } }}