pub mod condenser;
pub mod error;
pub mod event;
pub mod fork;
pub mod llm_condenser;
pub mod log;
pub mod replay;
pub mod store;
pub use condenser::{CondensationResult, Condenser};
pub use error::SessionError;
pub use event::{CompactionTier, SessionEvent, SessionEventEnvelope};
pub use fork::{ForkEngine, ForkResult};
pub use llm_condenser::LlmCondenser;
pub use log::SessionEventLog;
pub use replay::{ReconstructedState, ReplayEngine};
pub use store::{SessionFilter, SessionMetadata, SessionStatus, SessionStore};
#[must_use]
pub fn session_dir(data_dir: &std::path::Path, session_id: &str) -> std::path::PathBuf {
data_dir.join(session_id)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MigrationReport {
pub migrated: usize,
pub skipped: usize,
}
pub async fn migrate_legacy_session_layout(
data_dir: &std::path::Path,
) -> Result<MigrationReport, SessionError> {
let legacy_root = data_dir.join("sessions");
let mut entries = match tokio::fs::read_dir(&legacy_root).await {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(MigrationReport::default());
}
Err(e) => return Err(e.into()),
};
let mut report = MigrationReport::default();
while let Some(entry) = entries.next_entry().await? {
if !entry.file_type().await?.is_dir() {
continue;
}
let src = entry.path();
let dest = data_dir.join(entry.file_name());
if tokio::fs::try_exists(&dest).await? {
tracing::warn!(
src = %src.display(),
dest = %dest.display(),
"legacy session directory left in place: destination already exists (#5981)"
);
report.skipped += 1;
continue;
}
tokio::fs::rename(&src, &dest).await?;
report.migrated += 1;
}
if report.migrated > 0 || report.skipped > 0 {
tracing::info!(
migrated = report.migrated,
skipped = report.skipped,
"migrated legacy (#5981) session directory layout"
);
}
Ok(report)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::{MigrationReport, migrate_legacy_session_layout, session_dir};
#[test]
fn session_dir_does_not_double_nest() {
let dir = session_dir(Path::new(".zeph/sessions"), "abc-123");
assert_eq!(dir, Path::new(".zeph/sessions/abc-123"));
}
#[test]
fn session_dir_joins_arbitrary_data_dir() {
let dir = session_dir(Path::new("/var/lib/zeph/data"), "s1");
assert_eq!(dir, Path::new("/var/lib/zeph/data/s1"));
}
#[tokio::test]
async fn migrate_moves_legacy_session_dir_up_one_level() {
let data_dir = tempfile::tempdir().unwrap();
let legacy = data_dir.path().join("sessions").join("abc-123");
tokio::fs::create_dir_all(&legacy).await.unwrap();
tokio::fs::write(legacy.join("events.jsonl"), b"{}\n")
.await
.unwrap();
let report = migrate_legacy_session_layout(data_dir.path())
.await
.unwrap();
assert_eq!(
report,
MigrationReport {
migrated: 1,
skipped: 0
}
);
assert!(
data_dir
.path()
.join("abc-123")
.join("events.jsonl")
.exists()
);
assert!(!legacy.exists());
let second = migrate_legacy_session_layout(data_dir.path())
.await
.unwrap();
assert_eq!(second, MigrationReport::default());
}
#[tokio::test]
async fn migrate_is_noop_when_legacy_dir_is_empty() {
let data_dir = tempfile::tempdir().unwrap();
tokio::fs::create_dir_all(data_dir.path().join("sessions"))
.await
.unwrap();
let report = migrate_legacy_session_layout(data_dir.path())
.await
.unwrap();
assert_eq!(report, MigrationReport::default());
}
#[tokio::test]
async fn migrate_skips_when_destination_already_exists() {
let data_dir = tempfile::tempdir().unwrap();
let legacy = data_dir.path().join("sessions").join("abc-123");
tokio::fs::create_dir_all(&legacy).await.unwrap();
tokio::fs::write(legacy.join("events.jsonl"), b"old\n")
.await
.unwrap();
let dest = data_dir.path().join("abc-123");
tokio::fs::create_dir_all(&dest).await.unwrap();
tokio::fs::write(dest.join("events.jsonl"), b"new\n")
.await
.unwrap();
let report = migrate_legacy_session_layout(data_dir.path())
.await
.unwrap();
assert_eq!(
report,
MigrationReport {
migrated: 0,
skipped: 1
}
);
assert_eq!(
tokio::fs::read(dest.join("events.jsonl")).await.unwrap(),
b"new\n",
"destination must not be clobbered"
);
assert!(legacy.exists(), "legacy directory must be left in place");
}
#[tokio::test]
async fn migrate_is_noop_when_legacy_root_missing() {
let data_dir = tempfile::tempdir().unwrap();
let report = migrate_legacy_session_layout(data_dir.path())
.await
.unwrap();
assert_eq!(report, MigrationReport::default());
}
}