use super::{AgentOutputSink, util::safe_error_message};
use crate::{
output::OutputEvent,
sessions::{
Session, SessionEventKind, SessionReadDiagnostic, record_session_event,
try_record_session_event,
},
};
use serde_json::Value;
use std::path::Path;
pub(super) struct SessionPersistence<'a> {
session: Option<&'a Session>,
cwd: &'a Path,
warning_emitted: bool,
incomplete_marker_attempted: bool,
terminal_status_recorded: bool,
}
impl<'a> SessionPersistence<'a> {
pub(super) fn new(session: Option<&'a Session>, cwd: &'a Path) -> Self {
Self {
session,
cwd,
warning_emitted: false,
incomplete_marker_attempted: false,
terminal_status_recorded: false,
}
}
pub(super) fn is_degraded(&self) -> bool {
self.warning_emitted
}
pub(super) fn warn_once(
&mut self,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
error: &anyhow::Error,
) -> anyhow::Result<()> {
if !self.incomplete_marker_attempted {
self.incomplete_marker_attempted = true;
let _ = try_record_session_event(
self.session,
self.cwd,
SessionEventKind::TurnStatus,
serde_json::json!({"status":"incomplete"}),
);
}
if self.warning_emitted {
return Ok(());
}
self.warning_emitted = true;
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::Diagnostic {
level: "warning".to_string(),
message: format!(
"session persistence failed; future resume may be incomplete: {}",
safe_error_message(error)
),
})?;
}
Ok(())
}
pub(super) fn try_record(
&mut self,
kind: SessionEventKind,
payload: Value,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
) -> anyhow::Result<()> {
if let Err(error) = try_record_session_event(self.session, self.cwd, kind, payload) {
self.warn_once(output_sink, &error)?;
}
Ok(())
}
pub(super) fn record_terminal_status(
&mut self,
status: &str,
assistant_text: &str,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
) -> anyhow::Result<()> {
if self.terminal_status_recorded {
return Ok(());
}
self.terminal_status_recorded = true;
let _ = self.try_record(
SessionEventKind::TurnStatus,
serde_json::json!({"status": status, "assistant_text": assistant_text}),
output_sink,
);
Ok(())
}
pub(super) fn record_abort_recovery(
&mut self,
payload: Value,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
) -> anyhow::Result<()> {
self.try_record(SessionEventKind::AbortRecovery, payload, output_sink)
}
pub(super) fn record_provider_stream_trace(
&mut self,
payload: Value,
output_sink: &mut Option<&mut dyn AgentOutputSink>,
) -> anyhow::Result<()> {
self.try_record(SessionEventKind::ProviderStreamTrace, payload, output_sink)
}
pub(super) fn record_required(
&self,
kind: SessionEventKind,
payload: Value,
) -> anyhow::Result<()> {
record_session_event(self.session, self.cwd, kind, payload).map(|_| ())
}
}
pub(super) fn emit_replay_diagnostics(
output_sink: &mut Option<&mut dyn AgentOutputSink>,
diagnostics: &[SessionReadDiagnostic],
) -> anyhow::Result<()> {
if diagnostics.is_empty() {
return Ok(());
}
if let Some(sink) = output_sink.as_deref_mut() {
sink.output_event(OutputEvent::Diagnostic {
level: "warning".to_string(),
message: format!(
"session replay skipped {} unreadable JSONL line(s); continuing with valid session events only",
diagnostics.len()
),
})?;
}
Ok(())
}