use std::{
collections::VecDeque,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use rho_sdk::{
model::{Message, SemanticMessage},
SessionId, SessionSnapshot, SessionStore, SessionStoreFuture, UserInput,
};
use super::SessionHost;
use crate::{
added_dirs::AddedDirs, app::active_prompt::ActivePrompt, session::Session as StoredSession,
tools::todo::TodoState,
};
pub(super) type CreateStoredSession = Box<dyn Fn() -> anyhow::Result<StoredSession> + Send + Sync>;
pub(super) struct HostStorage {
cwd: PathBuf,
file: SessionFile,
}
enum SessionFile {
Unsaved(CreateStoredSession),
Saved(StoredSession),
}
impl HostStorage {
pub(super) fn deferred(
cwd: PathBuf,
id: String,
agent_id: String,
fingerprint: String,
) -> Self {
let file_cwd = cwd.clone();
Self::unsaved(
cwd,
Box::new(move || {
StoredSession::create_with_id(&file_cwd, &id, &agent_id, &fingerprint)
}),
)
}
pub(super) fn unsaved(cwd: PathBuf, create: CreateStoredSession) -> Self {
Self {
cwd,
file: SessionFile::Unsaved(create),
}
}
pub(super) fn saved(stored: StoredSession) -> Self {
Self {
cwd: stored.cwd().to_path_buf(),
file: SessionFile::Saved(stored),
}
}
pub(super) fn cwd(&self) -> &Path {
&self.cwd
}
pub(super) fn file(&self) -> Option<&StoredSession> {
match &self.file {
SessionFile::Saved(stored) => Some(stored),
SessionFile::Unsaved(_) => None,
}
}
}
impl SessionHost {
pub(super) fn stored(&mut self) -> anyhow::Result<&StoredSession> {
if let SessionFile::Unsaved(create) = &self.storage.file {
let stored = create()?;
self.built.tools.bind_recall(Some(&stored));
self.storage.file = SessionFile::Saved(stored);
}
match &self.storage.file {
SessionFile::Saved(stored) => Ok(stored),
SessionFile::Unsaved(_) => unreachable!("the file was created above"),
}
}
pub(super) fn install_turn_checkpoint(
&mut self,
display: &UserInput,
model: &UserInput,
) -> anyhow::Result<TurnCheckpoint> {
let stored = self.stored()?.clone();
let checkpoint = TurnCheckpoint(Arc::new(CheckpointStore {
stored,
todo: self.built.tools.todo_state(),
prompt: self.built.prompt.clone(),
added_dirs: self.added_dirs.clone(),
replaced: Arc::clone(&self.replaced),
display: Mutex::new(TurnDisplay::new(
Message::User(display.blocks().to_vec()),
Message::User(model.blocks().to_vec()),
self.built.session.history().len(),
)),
}));
self.built
.session
.set_checkpoint_store(Some(checkpoint.0.clone()))?;
Ok(checkpoint)
}
pub(super) fn persist_turn(
&mut self,
checkpoint: &TurnCheckpoint,
assistant_text: Option<&str>,
) -> anyhow::Result<()> {
if self.is_replaced() {
return Ok(());
}
let snapshot = self.durable_snapshot();
let mut display = checkpoint.0.lock_display();
let mut pending = display.pending_rows(snapshot.history(), SaveScope::Settle);
if !display.follows(snapshot.history()) {
pending.rows.extend(
assistant_text
.filter(|text| !text.is_empty())
.map(Message::assistant_text),
);
}
self.stored()?.save_snapshot(&snapshot, &pending.rows)?;
display.mark_written(pending);
Ok(())
}
pub(super) fn persist_failed_turn(&mut self, checkpoint: &TurnCheckpoint) {
if let Err(error) = self.persist_turn(checkpoint, None) {
tracing::warn!(%error, "could not save failed ACP turn");
}
}
pub(super) fn is_replaced(&self) -> bool {
self.replaced.load(Ordering::Acquire)
}
pub(super) fn durable_snapshot(&self) -> SessionSnapshot {
decorate(
&self.built.prompt,
&self.built.tools.todo_state(),
&self.added_dirs,
self.built.session.snapshot(),
)
}
}
fn decorate(
prompt: &ActivePrompt,
todo: &TodoState,
added_dirs: &AddedDirs,
snapshot: SessionSnapshot,
) -> SessionSnapshot {
added_dirs.decorate(todo.decorate(prompt.decorate(snapshot)))
}
pub(super) struct TurnCheckpoint(Arc<CheckpointStore>);
impl TurnCheckpoint {
pub(super) fn record_display(&self, input: &UserInput, shown: Message) {
self.0
.lock_display()
.applied
.push_back((Message::User(input.blocks().to_vec()), shown));
}
}
struct CheckpointStore {
stored: StoredSession,
todo: TodoState,
prompt: ActivePrompt,
added_dirs: AddedDirs,
replaced: Arc<AtomicBool>,
display: Mutex<TurnDisplay>,
}
impl CheckpointStore {
fn lock_display(&self) -> std::sync::MutexGuard<'_, TurnDisplay> {
self.display
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn save_now(&self, snapshot: SessionSnapshot) -> anyhow::Result<()> {
if self.replaced.load(Ordering::Acquire) {
return Ok(());
}
let snapshot = decorate(&self.prompt, &self.todo, &self.added_dirs, snapshot);
let mut display = self.lock_display();
let pending = display.pending_rows(snapshot.history(), SaveScope::Step);
self.stored.save_snapshot(&snapshot, &pending.rows)?;
display.mark_written(pending);
Ok(())
}
}
impl SessionStore for CheckpointStore {
fn load<'a>(&'a self, id: &'a SessionId) -> SessionStoreFuture<'a, Option<SessionSnapshot>> {
SessionStore::load(&self.stored, id)
}
fn save<'a>(&'a self, snapshot: SessionSnapshot) -> SessionStoreFuture<'a, ()> {
Box::pin(async move {
self.save_now(snapshot)
.map_err(|error| rho_sdk::Error::Persistence {
message: format!("{error:#}"),
})
})
}
}
#[derive(Debug)]
struct TurnDisplay {
unsaved_user: Option<Message>,
input: Message,
history_start: usize,
followed: Followed,
applied: VecDeque<(Message, Message)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Followed {
Tracking { written: usize },
Lost,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SaveScope {
Step,
Settle,
}
struct PendingRows {
rows: Vec<Message>,
applied_written: usize,
followed: Followed,
}
impl TurnDisplay {
fn new(user: Message, input: Message, history_start: usize) -> Self {
Self {
unsaved_user: Some(user),
input,
history_start,
followed: Followed::Tracking { written: 0 },
applied: VecDeque::new(),
}
}
fn pending_rows(&self, history: &[Message], scope: SaveScope) -> PendingRows {
let mut rows: Vec<Message> = self.unsaved_user.iter().cloned().collect();
let mut applied = self.applied.iter().peekable();
let Some((appended, written)) = self.appended(history) else {
let applied_written = match scope {
SaveScope::Step => 0,
SaveScope::Settle => {
rows.extend(applied.map(|(_, shown)| shown.clone()));
self.applied.len()
}
};
return PendingRows {
rows,
applied_written,
followed: Followed::Lost,
};
};
let mut taken = 0;
for row in appended {
if let Some((_, shown)) = applied.next_if(|(model, _)| model == row) {
rows.push(shown.clone());
} else if scope == SaveScope::Step && matches!(row.semantic(), SemanticMessage::User(_))
{
break;
} else {
rows.push(row.clone());
}
taken += 1;
}
PendingRows {
rows,
applied_written: self.applied.len() - applied.len(),
followed: Followed::Tracking {
written: written + taken,
},
}
}
fn mark_written(&mut self, pending: PendingRows) {
self.unsaved_user = None;
self.applied.drain(..pending.applied_written);
self.followed = pending.followed;
}
fn follows(&self, history: &[Message]) -> bool {
self.appended(history).is_some()
}
fn appended<'h>(&self, history: &'h [Message]) -> Option<(&'h [Message], usize)> {
let Followed::Tracking { written } = self.followed else {
return None;
};
if history.get(self.history_start) != Some(&self.input) {
return None;
}
let rows = history.get(self.history_start + 1 + written..)?;
Some((rows, written))
}
}
#[cfg(test)]
#[path = "session_host_persist_tests.rs"]
mod tests;