use std::path::Path;
use harn_session_store::{CreateSession, SessionStore, StoreError};
use crate::value::VmError;
#[derive(Debug)]
pub enum CanonicalForkError {
Admission(super::SessionOpenError),
Persistence(VmError),
}
#[derive(Clone, Debug)]
pub struct CanonicalForkResult {
pub session_id: String,
pub source_boundary: super::CanonicalSessionBoundary,
}
impl From<VmError> for CanonicalForkError {
fn from(error: VmError) -> Self {
Self::Persistence(error)
}
}
impl std::fmt::Display for CanonicalForkError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Admission(error) => std::fmt::Display::fmt(error, formatter),
Self::Persistence(error) => std::fmt::Display::fmt(error, formatter),
}
}
}
pub async fn fork_canonical(
store: &dyn SessionStore,
root: &Path,
source: &str,
boundary: Option<super::CanonicalSessionBoundary>,
destination: Option<String>,
) -> Result<Option<CanonicalForkResult>, CanonicalForkError> {
if !super::exists(source) {
return Ok(None);
}
ensure_canonical_parent(store, root, source).await?;
let events = store.read_all(source).await.map_err(store_error)?;
let hydrated = crate::agent_session_journal::hydrate_events(events.clone());
if !super::has_journal(source) {
hydrated.restore(source).map_err(VmError::Runtime)?;
}
let boundary = boundary.unwrap_or_else(|| {
events.last().map_or_else(
|| super::CanonicalSessionBoundary::empty(source),
super::CanonicalSessionBoundary::acknowledged,
)
});
boundary.validate(source, &events).map_err(store_error)?;
let event_id = boundary.event_id;
let copied = crate::agent_session_journal::hydrate_events(
events
.into_iter()
.filter(|event| event_id.is_some_and(|tip| event.event_id <= tip))
.collect(),
);
let child = super::fork(source, destination).map_err(CanonicalForkError::Admission)?;
let Some(child) = child else { return Ok(None) };
if let Err(error) = copied.restore(&child) {
super::close(&child);
return Err(VmError::Runtime(error).into());
}
if let Err(error) = store
.fork(source, boundary.clone(), Some(child.clone()))
.await
{
super::close(&child);
return Err(store_error(error));
}
Ok(Some(CanonicalForkResult {
session_id: child,
source_boundary: boundary,
}))
}
pub(super) async fn ensure_canonical_parent(
store: &dyn SessionStore,
root: &Path,
source: &str,
) -> Result<(), VmError> {
crate::agent_session_journal::flush(source).await?;
let result = match store.describe(source).await {
Ok(_) => return Ok(()),
Err(StoreError::NotFound(_)) if super::length(source) == Some(0) => store
.create(CreateSession {
id: Some(source.to_string()),
cwd: Some(root.to_string_lossy().into_owned()),
project_scope: Some(root.to_string_lossy().into_owned()),
parent_session_id: super::parent_id(source),
..CreateSession::default()
})
.await
.map(|_| ()),
Err(error) => Err(error),
};
result.map_err(|error| VmError::Runtime(format!("canonical session history: {error}")))
}
fn store_error(error: StoreError) -> CanonicalForkError {
CanonicalForkError::Persistence(VmError::Runtime(format!("canonical session fork: {error}")))
}