use std::io;
use std::sync::Arc;
use crate::session::task::TaskList;
use crate::session::types::Session;
use crate::session::PermissionAuditSeed;
pub fn append_missing_runtime_messages(session: &mut Session, durable: &Session) -> usize {
let mut seen = durable
.messages
.iter()
.map(|message| message.id.clone())
.collect::<std::collections::HashSet<_>>();
let missing = session
.messages
.iter()
.filter(|message| seen.insert(message.id.clone()))
.cloned()
.collect::<Vec<_>>();
let appended = missing.len();
session.messages = durable.messages.iter().cloned().chain(missing).collect();
appended
}
pub fn merge_session_inbox_admission(session: &mut Session, durable: &Session) {
let Some(durable_state) = durable.session_inbox_admission().cloned() else {
return;
};
session
.session_inbox_admission_mut()
.merge_from(&durable_state);
}
pub fn restore_missing_admitted_inbox_messages(session: &mut Session, durable: &Session) -> usize {
let admission = durable.session_inbox_admission();
let mut restored = 0;
for (durable_index, message) in durable.messages.iter().enumerate() {
let typed_marker = message
.metadata
.as_ref()
.and_then(|metadata| metadata.get("session_message"))
.is_some_and(|marker| {
marker.get("id").and_then(serde_json::Value::as_str) == Some(message.id.as_str())
&& marker
.get("target_session_id")
.and_then(serde_json::Value::as_str)
== Some(durable.id.as_str())
&& crate::SessionMessageId::parse(message.id.clone()).is_ok()
});
let recent_cursor = admission.is_some_and(|state| state.contains_str(&message.id));
if !(typed_marker || recent_cursor)
|| session
.messages
.iter()
.any(|current| current.id == message.id)
{
continue;
}
let insertion = durable.messages[..durable_index]
.iter()
.rev()
.find_map(|predecessor| {
session
.messages
.iter()
.position(|current| current.id == predecessor.id)
.map(|index| index + 1)
})
.or_else(|| {
durable.messages[durable_index + 1..]
.iter()
.find_map(|successor| {
session
.messages
.iter()
.position(|current| current.id == successor.id)
})
})
.unwrap_or(session.messages.len());
session.messages.insert(insertion, message.clone());
restored += 1;
}
restored
}
#[async_trait::async_trait]
pub trait RuntimeSessionPersistence: Send + Sync {
async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()>;
async fn seed_runtime_activation(&self, _session: &mut Session) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"runtime persistence does not support authoritative activation seeding",
))
}
async fn record_permission_posture_activation(
&self,
_session_id: &str,
_expected_audit_revision: Option<u64>,
_seed: &PermissionAuditSeed,
) -> io::Result<Option<Session>> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"runtime persistence does not support atomic permission posture activation",
))
}
async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
self.save_runtime_session(session).await
}
async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
self.load_runtime_session(session_id).await
}
async fn update_task_list_control_plane(
&self,
session_id: &str,
task_list: &TaskList,
version: &str,
) -> io::Result<bool> {
let Some(mut session) = self.load_runtime_session(session_id).await? else {
return Ok(false);
};
session.set_task_list(task_list.clone());
session.set_task_list_version_meta(version.to_string());
self.save_runtime_control_plane(&mut session).await?;
Ok(true)
}
async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
if let Some(durable) = self.load_runtime_session(&session.id).await? {
append_missing_runtime_messages(session, &durable);
merge_session_inbox_admission(session, &durable);
}
self.save_runtime_session(session).await
}
async fn load_runtime_session(&self, _session_id: &str) -> io::Result<Option<Session>> {
Ok(None)
}
async fn clear_legacy_pending_messages(
&self,
session_id: &str,
expected: &[serde_json::Value],
) -> io::Result<bool> {
let Some(mut latest) = self.load_runtime_session(session_id).await? else {
return Ok(false);
};
if latest.pending_injected_messages().as_deref() != Some(expected) {
return Ok(false);
}
latest.clear_pending_injected_messages();
self.save_runtime_session(&mut latest).await?;
Ok(true)
}
async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
let _ = (session_id, json_line);
Ok(())
}
}
#[async_trait::async_trait]
impl<T: RuntimeSessionPersistence + ?Sized> RuntimeSessionPersistence for Arc<T> {
async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()> {
(**self).save_runtime_session(session).await
}
async fn seed_runtime_activation(&self, session: &mut Session) -> io::Result<()> {
(**self).seed_runtime_activation(session).await
}
async fn record_permission_posture_activation(
&self,
session_id: &str,
expected_audit_revision: Option<u64>,
seed: &PermissionAuditSeed,
) -> io::Result<Option<Session>> {
(**self)
.record_permission_posture_activation(session_id, expected_audit_revision, seed)
.await
}
async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
(**self).save_runtime_control_plane(session).await
}
async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
(**self).load_runtime_control_plane(session_id).await
}
async fn update_task_list_control_plane(
&self,
session_id: &str,
task_list: &TaskList,
version: &str,
) -> io::Result<bool> {
(**self)
.update_task_list_control_plane(session_id, task_list, version)
.await
}
async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
(**self).checkpoint_runtime_session(session).await
}
async fn load_runtime_session(&self, session_id: &str) -> io::Result<Option<Session>> {
(**self).load_runtime_session(session_id).await
}
async fn clear_legacy_pending_messages(
&self,
session_id: &str,
expected: &[serde_json::Value],
) -> io::Result<bool> {
(**self)
.clear_legacy_pending_messages(session_id, expected)
.await
}
async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
(**self)
.append_token_usage_record(session_id, json_line)
.await
}
}