use super::*;
use meerkat_core::AgentExecutionSnapshot;
use meerkat_core::CommsCapabilityError;
use meerkat_core::ExternalToolSurfaceSnapshot;
use meerkat_core::PeerIngressRuntimeSnapshot;
use meerkat_core::Session;
use meerkat_core::ToolScopeSnapshot;
use meerkat_core::lifecycle::core_executor::CoreApplyOutput;
use meerkat_core::lifecycle::run_primitive::{RunApplyBoundary, TurnRequestContext};
use meerkat_core::lifecycle::run_receipt::RunBoundaryReceiptDraft;
use meerkat_core::service::StartTurnRequest;
use meerkat_core::service::{
SessionError, SessionServiceCommsExt, SessionServiceControlExt, SessionServiceHistoryExt,
};
use meerkat_core::{InputId, RunId};
#[cfg(feature = "runtime-adapter")]
use std::collections::HashMap;
#[cfg(feature = "runtime-adapter")]
use std::sync::{Mutex, OnceLock, Weak};
#[derive(Debug)]
#[non_exhaustive]
pub enum ResumeSessionLoad {
Active(Box<Session>),
Revivable(Box<Session>),
ArchivedNotRevivable {
runtime_state: Option<meerkat_runtime::RuntimeState>,
},
Absent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SessionResumeLifecycle {
NoCurrentDurableAuthority,
ContradictoryDurableAuthority {
runtime_state: Option<meerkat_runtime::RuntimeState>,
occurrence_generation: Option<u64>,
head_revision: Option<u64>,
},
Archived {
revivable: bool,
runtime_state: Option<meerkat_runtime::RuntimeState>,
head_revision: Option<u64>,
},
Active {
occurrence_generation: Option<u64>,
head_revision: Option<u64>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SessionResumeMaterialization {
Active,
Revivable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResumeRejectionKind {
Absent,
ArchivedNotRevivable,
CommittedBoundaryUnprovable,
AuthorityChangedDuringMaterialization,
ContradictoryDurableAuthority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResumeVerdictTerminality {
StableParkable,
TransientRetryable,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SessionResumeRejection {
pub session_id: SessionId,
pub lifecycle: SessionResumeLifecycle,
pub authority: Box<SessionResumeAuthority>,
pub kind: ResumeRejectionKind,
pub detail: String,
pub runtime_state: Option<meerkat_runtime::RuntimeState>,
pub terminality: ResumeVerdictTerminality,
}
impl SessionResumeRejection {
#[must_use]
pub fn into_mob_error(self) -> MobError {
let reason = match self.kind {
ResumeRejectionKind::Absent => crate::error::SessionResumeUnavailableReason::Absent,
ResumeRejectionKind::ArchivedNotRevivable => {
crate::error::SessionResumeUnavailableReason::ArchivedNotRevivable
}
ResumeRejectionKind::CommittedBoundaryUnprovable => {
crate::error::SessionResumeUnavailableReason::CommittedBoundaryUnprovable
}
ResumeRejectionKind::AuthorityChangedDuringMaterialization => {
crate::error::SessionResumeUnavailableReason::AuthorityChangedDuringMaterialization
}
ResumeRejectionKind::ContradictoryDurableAuthority => {
crate::error::SessionResumeUnavailableReason::ContradictoryDurableAuthority
}
};
MobError::SessionUnavailableForResume {
session_id: self.session_id.clone(),
reason,
runtime_state: self.runtime_state.map(|state| state.to_string()),
verdict: Some(Box::new(self)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SessionResumeAuthority {
pub observation: Option<meerkat_runtime::store::RuntimeSessionResumeObservation>,
}
impl SessionResumeAuthority {
#[must_use]
pub fn session_store_authority(
&self,
) -> Option<crate::identity::IdentitySessionStoreAuthority> {
self.observation
.as_ref()?
.session_authority()
.cloned()
.map(crate::identity::IdentitySessionStoreAuthority::from_runtime_authority)
}
#[must_use]
pub fn runtime_state(&self) -> Option<meerkat_runtime::RuntimeState> {
match self.observation.as_ref()?.lifecycle() {
meerkat_runtime::store::MachineLifecycleObservation::Decoded { record, .. } => {
record.runtime_state()
}
_ => None,
}
}
#[must_use]
pub fn occurrence_generation(&self) -> Option<u64> {
self.observation.as_ref()?.runtime_generation()
}
#[must_use]
pub fn head_revision(&self) -> Option<u64> {
self.observation.as_ref()?.session_store_revision()
}
#[must_use]
pub fn lifecycle(&self) -> SessionResumeLifecycle {
SessionResumeVerdict::lifecycle_from_authority(self)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum SessionResumeVerdict {
ResumeAuthorized {
lifecycle: SessionResumeLifecycle,
authority: SessionResumeAuthority,
materialization: SessionResumeMaterialization,
session: Box<Session>,
preparation: SessionResumePreparationReceipt,
},
Rejected(SessionResumeRejection),
}
#[derive(Debug)]
pub struct AuthorizedSessionResume {
pub lifecycle: SessionResumeLifecycle,
pub authority: SessionResumeAuthority,
pub materialization: SessionResumeMaterialization,
pub session: Box<Session>,
pub(crate) preparation: SessionResumePreparationReceipt,
}
#[derive(Debug)]
pub struct SessionResumePreparationReceipt {
session_id: SessionId,
authority: SessionResumeAuthority,
kind: SessionResumePreparationKind,
}
#[derive(Debug)]
enum SessionResumePreparationKind {
NonPersistent,
#[cfg(not(target_arch = "wasm32"))]
PersistentCommittedBoundary(meerkat_session::CommittedBoundaryResumePreparationReceipt),
}
impl SessionResumePreparationReceipt {
fn issue(
session_id: &SessionId,
authority: &SessionResumeAuthority,
kind: SessionResumePreparationKind,
) -> Self {
Self {
session_id: session_id.clone(),
authority: authority.clone(),
kind,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn from_persistent(
session_id: &SessionId,
authority: &SessionResumeAuthority,
preparation: meerkat_session::CommittedBoundaryResumePreparationReceipt,
) -> Self {
Self {
session_id: session_id.clone(),
authority: authority.clone(),
kind: SessionResumePreparationKind::PersistentCommittedBoundary(preparation),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn into_persistent_for(
self,
session_id: &SessionId,
) -> Result<meerkat_session::CommittedBoundaryResumePreparationReceipt, SessionError> {
if &self.session_id != session_id {
return Err(SessionError::Agent(
meerkat_core::error::AgentError::InternalError(format!(
"resume preparation receipt for '{}' cannot materialize session '{session_id}'",
self.session_id
)),
));
}
match self.kind {
SessionResumePreparationKind::PersistentCommittedBoundary(preparation) => {
Ok(preparation)
}
SessionResumePreparationKind::NonPersistent => Err(SessionError::Agent(
meerkat_core::error::AgentError::InternalError(format!(
"persistent resume for session '{session_id}' did not carry owner-issued committed-boundary preparation"
)),
)),
}
}
#[cfg(not(target_arch = "wasm32"))]
async fn advance_after_machine_prepare(
self,
prepared: &meerkat_runtime::PreparedSessionMaterialization,
) -> Result<Self, SessionError> {
let Self {
session_id,
authority,
kind,
} = self;
let kind = match kind {
SessionResumePreparationKind::NonPersistent => {
SessionResumePreparationKind::NonPersistent
}
SessionResumePreparationKind::PersistentCommittedBoundary(preparation) => {
SessionResumePreparationKind::PersistentCommittedBoundary(
preparation.advance_after_machine_prepare(prepared).await?,
)
}
};
Ok(Self {
session_id,
authority,
kind,
})
}
#[cfg(target_arch = "wasm32")]
async fn advance_after_machine_prepare(
self,
_prepared: &meerkat_runtime::PreparedSessionMaterialization,
) -> Result<Self, SessionError> {
Ok(self)
}
pub(crate) fn matches_authority(&self, authority: &SessionResumeAuthority) -> bool {
&self.authority == authority
}
}
impl SessionResumeVerdict {
pub fn into_authorized(self) -> Result<AuthorizedSessionResume, SessionResumeRejection> {
match self {
Self::ResumeAuthorized {
lifecycle,
authority,
materialization,
session,
preparation,
} => Ok(AuthorizedSessionResume {
lifecycle,
authority,
materialization,
session,
preparation,
}),
Self::Rejected(rejection) => Err(rejection),
}
}
fn from_authoritative_load_with_authority(
session_id: &SessionId,
load: ResumeSessionLoad,
authority: SessionResumeAuthority,
preparation: Option<SessionResumePreparationReceipt>,
) -> Result<Self, SessionError> {
let runtime_state = authority.runtime_state();
let occurrence_generation = authority.occurrence_generation();
let head_revision = authority.head_revision();
let authority_lifecycle = Self::lifecycle_from_authority(&authority);
if matches!(
&load,
ResumeSessionLoad::Active(_) | ResumeSessionLoad::Revivable(_)
) && authority
.observation
.as_ref()
.is_some_and(|observation| observation.session_authority().is_none())
{
return Ok(Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle: Self::lifecycle_from_authority(&authority),
runtime_state,
authority: Box::new(authority),
kind: ResumeRejectionKind::ContradictoryDurableAuthority,
detail: format!(
"session '{session_id}' materialized a body without store-issued session authority"
),
terminality: ResumeVerdictTerminality::StableParkable,
}));
}
match load {
ResumeSessionLoad::Active(session) => {
let preparation = preparation.ok_or_else(|| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"active resume for session '{session_id}' omitted owner-issued preparation"
)))
})?;
let lifecycle = match authority_lifecycle {
lifecycle @ SessionResumeLifecycle::Active { .. } => lifecycle,
SessionResumeLifecycle::NoCurrentDurableAuthority
if authority.observation.is_none() =>
{
SessionResumeLifecycle::Active {
occurrence_generation,
head_revision,
}
}
lifecycle => {
return Ok(Self::contradictory_durable_authority(
session_id,
authority,
lifecycle,
"active resume load disagrees with atomic durable lifecycle",
));
}
};
Ok(Self::ResumeAuthorized {
lifecycle,
authority,
materialization: SessionResumeMaterialization::Active,
session,
preparation,
})
}
ResumeSessionLoad::Revivable(session) => {
let preparation = preparation.ok_or_else(|| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"revivable resume for session '{session_id}' omitted owner-issued preparation"
)))
})?;
let lifecycle = match authority_lifecycle {
lifecycle @ SessionResumeLifecycle::Archived { .. } => lifecycle,
lifecycle @ SessionResumeLifecycle::Active { .. }
if runtime_state == Some(meerkat_runtime::RuntimeState::Retired) =>
{
lifecycle
}
lifecycle => {
return Ok(Self::contradictory_durable_authority(
session_id,
authority,
lifecycle,
"revivable resume load lacks matching atomic durable lifecycle",
));
}
};
Ok(Self::ResumeAuthorized {
lifecycle,
authority,
materialization: SessionResumeMaterialization::Revivable,
session,
preparation,
})
}
ResumeSessionLoad::ArchivedNotRevivable {
runtime_state: load_runtime_state,
} => {
let lifecycle = match authority_lifecycle {
lifecycle @ SessionResumeLifecycle::Archived {
revivable: false, ..
} => lifecycle,
lifecycle => {
return Ok(Self::contradictory_durable_authority(
session_id,
authority,
lifecycle,
"archived refusal disagrees with atomic durable lifecycle",
));
}
};
if load_runtime_state != runtime_state {
return Ok(Self::contradictory_durable_authority(
session_id,
authority,
lifecycle,
"archived refusal runtime state disagrees with atomic durable authority",
));
}
let state = runtime_state.map_or_else(
|| "<no runtime record>".to_string(),
|state| state.to_string(),
);
Ok(Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle,
authority: Box::new(authority),
kind: ResumeRejectionKind::ArchivedNotRevivable,
detail: format!(
"durable session '{session_id}' is archived and not revivable from runtime state {state}; the transcript is intact and preserved"
),
runtime_state,
terminality: if runtime_state == Some(meerkat_runtime::RuntimeState::Destroyed)
{
ResumeVerdictTerminality::StableParkable
} else {
ResumeVerdictTerminality::TransientRetryable
},
}))
}
ResumeSessionLoad::Absent => {
let positive_authority =
authority.observation.as_ref().is_some_and(|observation| {
observation.session_authority().is_some()
|| observation.catalog_entry().is_some()
|| !matches!(
observation.lifecycle(),
meerkat_runtime::store::MachineLifecycleObservation::Missing
)
});
if positive_authority {
Ok(Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle: Self::lifecycle_from_authority(&authority),
runtime_state: authority.runtime_state(),
authority: Box::new(authority),
kind: ResumeRejectionKind::ContradictoryDurableAuthority,
detail: format!(
"session '{session_id}' has durable authority or lifecycle rows but no materializable body"
),
terminality: ResumeVerdictTerminality::StableParkable,
}))
} else {
Ok(Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle: SessionResumeLifecycle::NoCurrentDurableAuthority,
authority: Box::new(authority),
kind: ResumeRejectionKind::Absent,
detail: format!("missing durable session snapshot for '{session_id}'"),
runtime_state: None,
terminality: ResumeVerdictTerminality::StableParkable,
}))
}
}
}
}
fn contradictory_durable_authority(
session_id: &SessionId,
authority: SessionResumeAuthority,
lifecycle: SessionResumeLifecycle,
detail: &str,
) -> Self {
let runtime_state = authority.runtime_state();
Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle,
authority: Box::new(authority),
kind: ResumeRejectionKind::ContradictoryDurableAuthority,
detail: format!("session '{session_id}' {detail}"),
runtime_state,
terminality: ResumeVerdictTerminality::StableParkable,
})
}
fn lifecycle_from_authority(authority: &SessionResumeAuthority) -> SessionResumeLifecycle {
let runtime_state = authority.runtime_state();
let head_revision = authority.head_revision();
match authority.observation.as_ref() {
None => SessionResumeLifecycle::NoCurrentDurableAuthority,
Some(observation)
if observation
.catalog_entry()
.and_then(|entry| entry.lifecycle_terminal())
.is_some_and(meerkat_core::SessionLifecycleTerminal::is_archived) =>
{
SessionResumeLifecycle::Archived {
revivable: matches!(
runtime_state,
None | Some(
meerkat_runtime::RuntimeState::Idle
| meerkat_runtime::RuntimeState::Retired
)
),
runtime_state,
head_revision,
}
}
Some(observation) if observation.session_authority().is_some() => {
SessionResumeLifecycle::Active {
occurrence_generation: authority.occurrence_generation(),
head_revision,
}
}
Some(observation)
if observation.catalog_entry().is_some()
|| !matches!(
observation.lifecycle(),
meerkat_runtime::store::MachineLifecycleObservation::Missing
) =>
{
SessionResumeLifecycle::ContradictoryDurableAuthority {
runtime_state,
occurrence_generation: authority.occurrence_generation(),
head_revision,
}
}
Some(_) => SessionResumeLifecycle::NoCurrentDurableAuthority,
}
}
fn committed_boundary_unprovable(
session_id: &SessionId,
_load: ResumeSessionLoad,
authority: SessionResumeAuthority,
detail: String,
) -> Result<Self, SessionError> {
let lifecycle = Self::lifecycle_from_authority(&authority);
let runtime_state = authority.runtime_state();
Ok(Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle,
authority: Box::new(authority),
kind: ResumeRejectionKind::CommittedBoundaryUnprovable,
detail,
runtime_state,
terminality: ResumeVerdictTerminality::StableParkable,
}))
}
pub(crate) fn authority_changed_during_materialization(
session_id: &SessionId,
authority: SessionResumeAuthority,
) -> Self {
let runtime_state = authority.runtime_state();
let lifecycle = Self::lifecycle_from_authority(&authority);
Self::Rejected(SessionResumeRejection {
session_id: session_id.clone(),
lifecycle,
authority: Box::new(authority),
kind: ResumeRejectionKind::AuthorityChangedDuringMaterialization,
detail: format!(
"durable authority for session '{session_id}' changed while its resume body was materialized"
),
runtime_state,
terminality: ResumeVerdictTerminality::TransientRetryable,
})
}
}
pub async fn materialize_nonpersistent_session_resume_verdict<S>(
session_service: &S,
session_id: &SessionId,
) -> Result<SessionResumeVerdict, SessionError>
where
S: MobSessionService + ?Sized,
{
if session_service.supports_persistent_sessions() {
return Err(SessionError::Unsupported(format!(
"persistent session service must issue an owner-prepared resume verdict for session '{session_id}'"
)));
}
materialize_nonpersistent_session_resume_verdict_inner(session_service, session_id).await
}
#[cfg(test)]
pub(crate) async fn materialize_nonpersistent_session_resume_verdict_unchecked<S>(
session_service: &S,
session_id: &SessionId,
) -> Result<SessionResumeVerdict, SessionError>
where
S: MobSessionService + ?Sized,
{
materialize_nonpersistent_session_resume_verdict_inner(session_service, session_id).await
}
async fn materialize_nonpersistent_session_resume_verdict_inner<S>(
session_service: &S,
session_id: &SessionId,
) -> Result<SessionResumeVerdict, SessionError>
where
S: MobSessionService + ?Sized,
{
let before = session_service
.observe_session_resume_authority(session_id)
.await?;
let load = session_service.load_session_for_resume(session_id).await?;
let authority = session_service
.observe_session_resume_authority(session_id)
.await?;
if before != authority {
return Ok(
SessionResumeVerdict::authority_changed_during_materialization(session_id, authority),
);
}
let preparation = SessionResumePreparationReceipt::issue(
session_id,
&authority,
SessionResumePreparationKind::NonPersistent,
);
SessionResumeVerdict::from_authoritative_load_with_authority(
session_id,
load,
authority,
Some(preparation),
)
}
#[cfg(feature = "runtime-adapter")]
pub(crate) enum SessionActorMaterializationRoute {
Fresh,
Resume {
preparation: SessionResumePreparationReceipt,
},
Revivable {
authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
preparation: SessionResumePreparationReceipt,
},
AttachedActorRecovery {
preparation: SessionResumePreparationReceipt,
},
}
#[cfg(feature = "runtime-adapter")]
impl SessionActorMaterializationRoute {
#[must_use]
pub(crate) fn is_revivable(&self) -> bool {
matches!(self, Self::Revivable { .. })
}
pub(crate) async fn advance_resume_preparation_after_machine_prepare(
self,
prepared: &meerkat_runtime::PreparedSessionMaterialization,
) -> Result<Self, SessionError> {
match self {
Self::Fresh => Ok(Self::Fresh),
Self::Resume { preparation } => Ok(Self::Resume {
preparation: preparation.advance_after_machine_prepare(prepared).await?,
}),
Self::Revivable {
authorization,
preparation,
} => Ok(Self::Revivable {
authorization,
preparation: preparation.advance_after_machine_prepare(prepared).await?,
}),
Self::AttachedActorRecovery { preparation } => Ok(Self::AttachedActorRecovery {
preparation: preparation.advance_after_machine_prepare(prepared).await?,
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PersistedSessionAuthorityReadCost {
Unsupported,
Bounded,
}
fn build_runtime_receipt(
run_id: RunId,
boundary: RunApplyBoundary,
contributing_input_ids: Vec<InputId>,
session: &Session,
) -> Result<RunBoundaryReceiptDraft, SessionError> {
let conversation_digest = session.transcript_content_digest().map_err(|err| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"failed to digest session for runtime receipt: {err}"
)))
})?;
Ok(RunBoundaryReceiptDraft {
run_id,
boundary,
contributing_input_ids,
conversation_digest: Some(conversation_digest),
message_count: session.messages().len(),
})
}
#[cfg(feature = "runtime-adapter")]
fn ephemeral_runtime_adapter_cache()
-> &'static Mutex<HashMap<usize, Weak<meerkat_runtime::MeerkatMachine>>> {
static CACHE: OnceLock<Mutex<HashMap<usize, Weak<meerkat_runtime::MeerkatMachine>>>> =
OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(all(not(target_arch = "wasm32"), feature = "runtime-adapter"))]
fn persistent_runtime_adapter_cache()
-> &'static Mutex<HashMap<usize, Weak<meerkat_runtime::MeerkatMachine>>> {
static CACHE: OnceLock<Mutex<HashMap<usize, Weak<meerkat_runtime::MeerkatMachine>>>> =
OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(feature = "runtime-adapter")]
fn cached_runtime_adapter(
cache: &'static Mutex<HashMap<usize, Weak<meerkat_runtime::MeerkatMachine>>>,
key: usize,
init: impl FnOnce() -> Arc<meerkat_runtime::MeerkatMachine>,
) -> Arc<meerkat_runtime::MeerkatMachine> {
let mut cache = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
cache.retain(|_, adapter| adapter.strong_count() > 0);
if let Some(existing) = cache.get(&key).and_then(Weak::upgrade) {
return existing;
}
let adapter = init();
cache.insert(key, Arc::downgrade(&adapter));
adapter
}
#[cfg(feature = "runtime-adapter")]
pub(crate) async fn retire_runtime_session_for_archive(
runtime_adapter: &meerkat_runtime::MeerkatMachine,
session_id: &SessionId,
) -> Result<(), SessionError> {
let runtime_id = meerkat_runtime::LogicalRuntimeId::for_session(session_id);
match meerkat_runtime::RuntimeControlPlane::retire(runtime_adapter, &runtime_id).await {
Ok(_) => Ok(()),
Err(meerkat_runtime::RuntimeControlPlaneError::NotFound(_)) => {
runtime_adapter
.register_session(session_id.clone())
.await
.map_err(|error| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"machine archive register before retire failed: {error}"
)))
})?;
meerkat_runtime::RuntimeControlPlane::retire(runtime_adapter, &runtime_id)
.await
.map(|_| ())
.map_err(|error| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"machine archive retire failed after registration: {error}"
)))
})
}
Err(error) => Err(SessionError::Agent(
meerkat_core::error::AgentError::InternalError(format!(
"machine archive retire failed: {error}"
)),
)),
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
pub trait MobSessionService:
SessionServiceCommsExt + SessionServiceControlExt + SessionServiceHistoryExt
{
async fn create_session_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
) -> Result<meerkat_core::RunResult, SessionError>;
async fn create_session_with_actor_witness_under_runtime_turn_boundary(
&self,
_req: meerkat_core::service::CreateSessionRequest,
_resume_preparation: Option<SessionResumePreparationReceipt>,
_actor_witness_slot: &meerkat_session::LiveSessionActorWitnessSlot,
) -> Result<meerkat_core::RunResult, SessionError> {
Err(SessionError::Unsupported(
"session service cannot publish exact actor identity during boundary-owned create"
.into(),
))
}
#[cfg(feature = "runtime-adapter")]
async fn create_session_with_machine_archived_resume_authority(
&self,
_req: meerkat_core::service::CreateSessionRequest,
_authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
) -> Result<meerkat_core::RunResult, SessionError> {
Err(SessionError::Unsupported(
"session service does not support machine-authorized archived resume".into(),
))
}
#[cfg(feature = "runtime-adapter")]
async fn create_session_with_machine_archived_resume_authority_under_runtime_turn_boundary(
&self,
_req: meerkat_core::service::CreateSessionRequest,
_authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
) -> Result<meerkat_core::RunResult, SessionError> {
Err(SessionError::Unsupported(
"session service does not support boundary-owned machine-authorized archived resume"
.into(),
))
}
#[cfg(feature = "runtime-adapter")]
async fn create_session_with_machine_archived_resume_authority_and_actor_witness_under_runtime_turn_boundary(
&self,
_req: meerkat_core::service::CreateSessionRequest,
_authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
_resume_preparation: SessionResumePreparationReceipt,
_actor_witness_slot: &meerkat_session::LiveSessionActorWitnessSlot,
) -> Result<meerkat_core::RunResult, SessionError> {
Err(SessionError::Unsupported(
"session service does not support exact-actor boundary-owned machine-authorized archived resume"
.into(),
))
}
#[cfg(feature = "runtime-adapter")]
async fn authorize_revivable_retired_session(
&self,
_session_id: &SessionId,
_authority: meerkat_runtime::PreparedArchivedResumeCommitLease,
) -> Result<meerkat_runtime::AuthorizedArchivedResumeCommitLease, SessionError> {
Err(SessionError::Unsupported(
"session service does not support exact retired-session authorization".into(),
))
}
async fn subscribe_session_events(
&self,
session_id: &SessionId,
) -> Result<EventStream, StreamError> {
<Self as SessionService>::subscribe_session_events(self, session_id).await
}
fn supports_persistent_sessions(&self) -> bool {
false
}
fn persisted_session_authority_read_cost(&self) -> PersistedSessionAuthorityReadCost {
PersistedSessionAuthorityReadCost::Unsupported
}
async fn observe_persisted_session_authority(
&self,
session_id: &SessionId,
) -> Result<Option<crate::identity::IdentitySessionStoreAuthority>, SessionError> {
Err(SessionError::Unsupported(format!(
"session service cannot observe exact persisted authority for {session_id}"
)))
}
async fn live_session_actor_registered(
&self,
session_id: &SessionId,
) -> Result<bool, SessionError> {
<Self as SessionService>::has_live_session(self, session_id).await
}
#[cfg(feature = "runtime-adapter")]
fn runtime_adapter(&self) -> Option<Arc<meerkat_runtime::MeerkatMachine>> {
None
}
#[cfg(feature = "runtime-adapter")]
fn supports_runtime_turn_apply(&self) -> bool {
false
}
#[cfg(feature = "runtime-adapter")]
async fn interrupt_with_machine_authority(
&self,
session_id: &SessionId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(format!(
"interrupt for runtime-backed mob session {session_id} must be implemented by the machine-owned session service"
)))
}
#[cfg(feature = "runtime-adapter")]
async fn interrupt_run_with_machine_authority(
&self,
session_id: &SessionId,
_expected_run_id: &RunId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<bool, SessionError> {
Err(SessionError::Unsupported(format!(
"exact-run interrupt for runtime-backed mob session {session_id} must be implemented by the machine-owned session service"
)))
}
#[cfg(feature = "runtime-adapter")]
async fn cancel_after_boundary_with_machine_authority(
&self,
session_id: &SessionId,
_expected_run_id: &RunId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(format!(
"cancel_after_boundary for runtime-backed mob session {session_id} must be implemented by the machine-owned session service"
)))
}
#[cfg(feature = "runtime-adapter")]
async fn cancel_current_after_boundary_with_machine_authority(
&self,
session_id: &SessionId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(format!(
"current-run cancel_after_boundary for runtime-backed mob session {session_id} must be implemented by the machine-owned session service"
)))
}
async fn execution_snapshot(
&self,
_session_id: &SessionId,
) -> Result<Option<AgentExecutionSnapshot>, SessionError> {
Ok(None)
}
async fn tool_scope_snapshot(
&self,
_session_id: &SessionId,
) -> Result<Option<ToolScopeSnapshot>, SessionError> {
Ok(None)
}
async fn external_tool_surface_snapshot(
&self,
_session_id: &SessionId,
) -> Result<Option<ExternalToolSurfaceSnapshot>, SessionError> {
Ok(None)
}
async fn peer_ingress_runtime_snapshot(
&self,
_session_id: &SessionId,
) -> Result<Option<PeerIngressRuntimeSnapshot>, SessionError> {
Ok(None)
}
async fn session_known_to_archive_authority(
&self,
_session_id: &SessionId,
) -> Result<bool, SessionError> {
Ok(true)
}
async fn session_belongs_to_mob(
&self,
_session_id: &SessionId,
_mob_id: &crate::ids::MobId,
) -> bool {
false
}
async fn load_persisted_session(
&self,
_session_id: &SessionId,
) -> Result<Option<Session>, SessionError> {
Ok(None)
}
async fn fork_persisted_session(
&self,
_source_session_id: &SessionId,
_message_count: Option<usize>,
_tool_access_policy: Option<meerkat_core::ops::ToolAccessPolicy>,
_target: meerkat_core::DurableSessionForkTarget,
) -> Result<meerkat_core::SessionForkResult, SessionError> {
Err(SessionError::Unsupported(
"session service does not expose durable transcript fork authority".into(),
))
}
async fn load_revivable_retired_session(
&self,
_session_id: &SessionId,
) -> Result<Option<Session>, SessionError> {
Ok(None)
}
async fn load_session_for_resume(
&self,
session_id: &SessionId,
) -> Result<ResumeSessionLoad, SessionError>;
async fn observe_session_resume_authority(
&self,
session_id: &SessionId,
) -> Result<SessionResumeAuthority, SessionError>;
async fn revalidate_session_resume_authority(
&self,
session_id: &SessionId,
expected: &SessionResumeAuthority,
) -> Result<Result<(), SessionResumeRejection>, SessionError> {
let current = self.observe_session_resume_authority(session_id).await?;
if ¤t == expected {
Ok(Ok(()))
} else {
let SessionResumeVerdict::Rejected(rejection) =
SessionResumeVerdict::authority_changed_during_materialization(session_id, current)
else {
unreachable!("authority-change constructor always rejects")
};
Ok(Err(rejection))
}
}
async fn materialize_session_resume_verdict(
&self,
session_id: &SessionId,
) -> Result<SessionResumeVerdict, SessionError>;
async fn load_persisted_session_metadata(
&self,
session_id: &SessionId,
) -> Result<Option<meerkat_core::PersistedSessionMetadataView>, SessionError> {
let Some(session) = self.load_persisted_session(session_id).await? else {
return Ok(None);
};
meerkat_core::PersistedSessionMetadataView::try_from_session(&session)
.map(Some)
.map_err(|err| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"session {session_id} durable metadata failed typed restore: {err}"
)))
})
}
async fn archive_with_mob_lifecycle_authority(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
#[cfg(feature = "runtime-adapter")]
if self.runtime_adapter().is_some() {
return Err(SessionError::Unsupported(format!(
"archive for runtime-backed mob session {session_id} must be implemented by the machine-owned session service"
)));
}
<Self as SessionService>::archive(self, session_id).await
}
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError>;
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary_before(
&self,
session_id: &SessionId,
_deadline: meerkat_core::time_compat::Instant,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(format!(
"session service must implement deadline-aware mob archive for session {session_id}"
)))
}
#[cfg(feature = "runtime-adapter")]
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary_and_hook_before(
&self,
session_id: &SessionId,
deadline: meerkat_core::time_compat::Instant,
post_commit_hook: Option<Arc<dyn meerkat_runtime::MachineSessionArchivePostCommitHook>>,
) -> Result<(), SessionError> {
if post_commit_hook.is_some() {
return Err(SessionError::Unsupported(format!(
"session service cannot run a pre-retire archive hook for session {session_id}"
)));
}
self.archive_with_mob_lifecycle_authority_under_runtime_turn_boundary_before(
session_id, deadline,
)
.await
}
async fn apply_runtime_turn(
&self,
_session_id: &SessionId,
_run_id: RunId,
_req: StartTurnRequest,
_boundary: RunApplyBoundary,
_contributing_input_ids: Vec<InputId>,
) -> Result<CoreApplyOutput, SessionError> {
Err(SessionError::Agent(
meerkat_core::error::AgentError::InternalError(
"runtime-backed apply is unavailable for this session service".into(),
),
))
}
async fn prepare_transient_turn_context_for_active_turn(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
contexts: Vec<TurnRequestContext>,
) -> Result<meerkat_core::CoreBoundaryStageOutput, meerkat_core::CoreBoundaryStageError> {
let _ = (session_id, expected_run_id, contexts);
Err(meerkat_core::CoreBoundaryStageError::unavailable(
"session service does not support exact active-turn boundary preparation",
))
}
async fn checkpoint_committed_runtime_session_snapshot(
&self,
_session_id: &SessionId,
_session_snapshot: Arc<Vec<u8>>,
) -> Result<(), SessionError> {
Ok(())
}
async fn acquire_runtime_turn_finalization_guard(
&self,
_session_id: &SessionId,
) -> Result<Box<dyn meerkat_core::lifecycle::CoreExecutorTurnFinalizationGuard>, SessionError>
{
Ok(Box::new(()))
}
async fn checkpoint_committed_runtime_session_snapshot_under_turn_finalization_boundary(
&self,
session_id: &SessionId,
session_snapshot: Arc<Vec<u8>>,
) -> Result<(), SessionError> {
self.checkpoint_committed_runtime_session_snapshot(session_id, session_snapshot)
.await
}
async fn acknowledge_committed_runtime_session_boundary_under_turn_finalization_boundary(
&self,
session_id: &SessionId,
authority: &meerkat_core::CommittedSessionBoundaryAuthority,
) -> Result<(), SessionError>;
async fn discard_live_session_after_runtime_stop_terminalized(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
self.discard_live_session(session_id).await
}
async fn discard_live_session_after_runtime_stop_terminalized_under_turn_finalization_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
self.discard_live_session_after_runtime_stop_terminalized(session_id)
.await
}
async fn publish_interaction_terminals_for_actor(
&self,
_actor_witness: &meerkat_session::LiveSessionActorWitness,
events: &[meerkat_core::event::AgentEvent],
) -> Result<
Vec<meerkat_core::lifecycle::core_executor::CoreInteractionTerminalPublicationReceipt>,
SessionError,
> {
if events.is_empty() {
return Ok(Vec::new());
}
Err(SessionError::Unsupported(
"exact interaction terminal publication requires actor-incarnation authority"
.to_string(),
))
}
async fn discard_live_session(&self, _session_id: &SessionId) -> Result<(), SessionError> {
Ok(())
}
async fn discard_live_session_under_runtime_turn_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError>;
async fn discard_live_session_actor_under_runtime_turn_boundary(
&self,
witness: &meerkat_session::LiveSessionActorWitness,
) -> Result<bool, SessionError> {
Err(SessionError::Unsupported(format!(
"session service cannot discard exact live actor for {}",
witness.session_id()
)))
}
async fn discard_live_session_actor_after_durability_reload_required(
&self,
witness: &meerkat_session::LiveSessionActorWitness,
) -> Result<bool, SessionError> {
Err(SessionError::Unsupported(format!(
"session service cannot run durability-reload actor discard for {}",
witness.session_id()
)))
}
async fn await_event_projection_drain(
&self,
session_id: &SessionId,
) -> Result<bool, SessionError> {
let _ = session_id;
Ok(false)
}
async fn cancel_all_checkpointers(&self) {}
async fn rearm_all_checkpointers(&self) {}
}
#[cfg(feature = "runtime-adapter")]
pub(crate) async fn execute_session_actor_materialization_under_runtime_turn_boundary(
session_service: &dyn MobSessionService,
req: meerkat_core::service::CreateSessionRequest,
route: SessionActorMaterializationRoute,
actor_witness_slot: &meerkat_session::LiveSessionActorWitnessSlot,
) -> Result<meerkat_core::RunResult, SessionError> {
match route {
SessionActorMaterializationRoute::Fresh => {
session_service
.create_session_with_actor_witness_under_runtime_turn_boundary(
req,
None,
actor_witness_slot,
)
.await
}
SessionActorMaterializationRoute::Resume { preparation }
| SessionActorMaterializationRoute::AttachedActorRecovery { preparation } => {
session_service
.create_session_with_actor_witness_under_runtime_turn_boundary(
req,
Some(preparation),
actor_witness_slot,
)
.await
}
SessionActorMaterializationRoute::Revivable {
authorization,
preparation,
} => {
session_service
.create_session_with_machine_archived_resume_authority_and_actor_witness_under_runtime_turn_boundary(
req,
authorization,
preparation,
actor_witness_slot,
)
.await
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl<B> MobSessionService for meerkat_session::EphemeralSessionService<B>
where
B: meerkat_session::SessionAgentBuilder + 'static,
{
async fn materialize_session_resume_verdict(
&self,
session_id: &SessionId,
) -> Result<SessionResumeVerdict, SessionError> {
materialize_nonpersistent_session_resume_verdict(self, session_id).await
}
async fn create_session_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
) -> Result<meerkat_core::RunResult, SessionError> {
<Self as meerkat_core::service::SessionService>::create_session(self, req).await
}
async fn observe_session_resume_authority(
&self,
_session_id: &SessionId,
) -> Result<SessionResumeAuthority, SessionError> {
Ok(SessionResumeAuthority::default())
}
async fn load_session_for_resume(
&self,
session_id: &SessionId,
) -> Result<ResumeSessionLoad, SessionError> {
if let Some(session) = self.load_persisted_session(session_id).await? {
return Ok(ResumeSessionLoad::Active(Box::new(session)));
}
if let Some(session) = self.load_revivable_retired_session(session_id).await? {
return Ok(ResumeSessionLoad::Revivable(Box::new(session)));
}
Ok(ResumeSessionLoad::Absent)
}
async fn create_session_with_actor_witness_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
_resume_preparation: Option<SessionResumePreparationReceipt>,
actor_witness_slot: &meerkat_session::LiveSessionActorWitnessSlot,
) -> Result<meerkat_core::RunResult, SessionError> {
#[cfg(feature = "runtime-adapter")]
let mut actor_materialization_permit = if let Some(bindings) =
req.build
.as_ref()
.and_then(|build| match &build.runtime_build_mode {
meerkat_core::RuntimeBuildMode::SessionOwned(bindings) => Some(bindings),
meerkat_core::RuntimeBuildMode::StandaloneEphemeral => None,
}) {
match meerkat_runtime::begin_session_runtime_actor_materialization(bindings) {
Ok(permit) => Some(permit),
Err(meerkat_runtime::RuntimeActorMaterializationError::RegistrationClosed) => {
return Err(SessionError::NotFound {
id: bindings.session_id().clone(),
});
}
Err(meerkat_runtime::RuntimeActorMaterializationError::InvalidAuthority(
reason,
)) => {
return Err(SessionError::Agent(
meerkat_core::error::AgentError::InternalError(reason),
));
}
}
} else {
None
};
let (result, actor_witness) =
meerkat_session::EphemeralSessionService::<B>::create_session_with_admission_and_witness(
self,
req,
None,
Some(actor_witness_slot),
)
.await?;
#[cfg(feature = "runtime-adapter")]
if let Some(permit) = actor_materialization_permit.take()
&& let Err(error) = permit.commit()
{
let cleanup =
meerkat_session::EphemeralSessionService::<B>::discard_live_session_actor(
self,
&actor_witness,
)
.await;
return Err(SessionError::Agent(
meerkat_core::error::AgentError::InternalError(match cleanup {
Ok(_) => format!(
"runtime actor materialization commit failed for session {}: {error}",
result.session_id
),
Err(cleanup_error) => format!(
"runtime actor materialization commit failed for session {}: {error}; exact actor cleanup also failed: {cleanup_error}",
result.session_id
),
}),
));
}
#[cfg(not(feature = "runtime-adapter"))]
let _ = actor_witness;
Ok(result)
}
fn supports_persistent_sessions(&self) -> bool {
false
}
async fn live_session_actor_registered(
&self,
session_id: &SessionId,
) -> Result<bool, SessionError> {
Ok(
meerkat_session::EphemeralSessionService::<B>::live_session_actor_registered(
self, session_id,
)
.await,
)
}
#[cfg(feature = "runtime-adapter")]
fn runtime_adapter(&self) -> Option<Arc<meerkat_runtime::MeerkatMachine>> {
let key = std::ptr::from_ref(self) as usize;
Some(cached_runtime_adapter(
ephemeral_runtime_adapter_cache(),
key,
|| Arc::new(meerkat_runtime::MeerkatMachine::ephemeral()),
))
}
#[cfg(feature = "runtime-adapter")]
fn supports_runtime_turn_apply(&self) -> bool {
true
}
async fn acquire_runtime_turn_finalization_guard(
&self,
session_id: &SessionId,
) -> Result<Box<dyn meerkat_core::lifecycle::CoreExecutorTurnFinalizationGuard>, SessionError>
{
Ok(Box::new(
meerkat_session::EphemeralSessionService::<B>::acquire_runtime_turn_finalization_guard(
self, session_id,
)
.await,
))
}
#[cfg(feature = "runtime-adapter")]
async fn interrupt_with_machine_authority(
&self,
session_id: &SessionId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
meerkat_core::service::SessionService::interrupt(self, session_id).await
}
#[cfg(feature = "runtime-adapter")]
async fn interrupt_run_with_machine_authority(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<bool, SessionError> {
meerkat_session::EphemeralSessionService::<B>::interrupt_run_if_current(
self,
session_id,
expected_run_id,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn cancel_after_boundary_with_machine_authority(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
meerkat_core::service::SessionService::cancel_after_boundary_for_run(
self,
session_id,
expected_run_id,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn cancel_current_after_boundary_with_machine_authority(
&self,
session_id: &SessionId,
_authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
meerkat_core::service::SessionService::cancel_after_boundary(self, session_id).await
}
async fn archive_with_mob_lifecycle_authority(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
<Self as SessionService>::read(self, session_id).await?;
#[cfg(feature = "runtime-adapter")]
if let Some(runtime_adapter) = self.runtime_adapter() {
retire_runtime_session_for_archive(runtime_adapter.as_ref(), session_id).await?;
}
<Self as SessionService>::archive(self, session_id).await
}
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
self.archive_with_mob_lifecycle_authority(session_id).await
}
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary_before(
&self,
session_id: &SessionId,
_deadline: meerkat_core::time_compat::Instant,
) -> Result<(), SessionError> {
self.archive_with_mob_lifecycle_authority_under_runtime_turn_boundary(session_id)
.await
}
async fn execution_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<AgentExecutionSnapshot>, SessionError> {
meerkat_session::EphemeralSessionService::<B>::execution_snapshot(self, session_id).await
}
async fn load_persisted_session(
&self,
session_id: &SessionId,
) -> Result<Option<Session>, SessionError> {
match meerkat_session::EphemeralSessionService::<B>::export_session(self, session_id).await
{
Ok(session) => Ok(Some(session)),
Err(SessionError::NotFound { .. }) => Ok(None),
Err(error) => Err(error),
}
}
async fn tool_scope_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<ToolScopeSnapshot>, SessionError> {
meerkat_session::EphemeralSessionService::<B>::tool_scope_snapshot(self, session_id).await
}
async fn external_tool_surface_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<ExternalToolSurfaceSnapshot>, SessionError> {
meerkat_session::EphemeralSessionService::<B>::external_tool_surface_snapshot(
self, session_id,
)
.await
}
async fn peer_ingress_runtime_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<PeerIngressRuntimeSnapshot>, SessionError> {
let Some(runtime) = self.comms_runtime(session_id).await else {
return Ok(None);
};
match runtime.peer_ingress_runtime_snapshot().await {
Ok(snapshot) => Ok(Some(snapshot)),
Err(CommsCapabilityError::Unsupported(_)) => Ok(None),
Err(error) => Err(SessionError::Unsupported(error.to_string())),
}
}
async fn subscribe_session_events(
&self,
session_id: &SessionId,
) -> Result<EventStream, StreamError> {
meerkat_session::EphemeralSessionService::<B>::subscribe_session_events(self, session_id)
.await
}
async fn discard_live_session(&self, session_id: &SessionId) -> Result<(), SessionError> {
meerkat_session::EphemeralSessionService::<B>::discard_live_session(self, session_id).await
}
async fn discard_live_session_under_runtime_turn_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
meerkat_session::EphemeralSessionService::<B>::discard_live_session(self, session_id).await
}
async fn discard_live_session_actor_under_runtime_turn_boundary(
&self,
witness: &meerkat_session::LiveSessionActorWitness,
) -> Result<bool, SessionError> {
meerkat_session::EphemeralSessionService::<B>::discard_live_session_actor(self, witness)
.await
}
async fn discard_live_session_actor_after_durability_reload_required(
&self,
witness: &meerkat_session::LiveSessionActorWitness,
) -> Result<bool, SessionError> {
meerkat_session::EphemeralSessionService::<B>::discard_live_session_actor(self, witness)
.await
}
async fn apply_runtime_turn(
&self,
session_id: &SessionId,
run_id: RunId,
req: StartTurnRequest,
boundary: RunApplyBoundary,
contributing_input_ids: Vec<InputId>,
) -> Result<CoreApplyOutput, SessionError> {
let run_result = meerkat_session::EphemeralSessionService::<B>::start_turn_under_runtime_turn_finalization_boundary(
self,
session_id,
req,
)
.await?;
let session =
meerkat_session::EphemeralSessionService::<B>::export_session(self, session_id).await?;
let receipt = build_runtime_receipt(run_id, boundary, contributing_input_ids, &session)?;
CoreApplyOutput::with_run_result(receipt, None, run_result)
.with_session(std::sync::Arc::new(session))
.map_err(|err| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"failed to seal typed session snapshot for runtime commit: {err}"
)))
})
}
async fn prepare_transient_turn_context_for_active_turn(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
contexts: Vec<TurnRequestContext>,
) -> Result<meerkat_core::CoreBoundaryStageOutput, meerkat_core::CoreBoundaryStageError> {
let prepared =
meerkat_session::EphemeralSessionService::<B>::prepare_transient_turn_context_for_active_turn(
self,
session_id,
expected_run_id,
contexts,
)
.await?;
Ok(prepared.into_stage_output(None))
}
async fn acknowledge_committed_runtime_session_boundary_under_turn_finalization_boundary(
&self,
_session_id: &SessionId,
_authority: &meerkat_core::CommittedSessionBoundaryAuthority,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"ephemeral session service cannot acknowledge store-owned runtime boundaries"
.to_string(),
))
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl<B> MobSessionService for meerkat_session::PersistentSessionService<B>
where
B: meerkat_session::SessionAgentBuilder + 'static,
{
async fn create_session_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
) -> Result<meerkat_core::RunResult, SessionError> {
let admission = self.reserve_create_session_admission().await?;
self.create_session_with_reserved_admission_under_runtime_turn_boundary(req, admission)
.await
}
async fn materialize_session_resume_verdict(
&self,
session_id: &SessionId,
) -> Result<SessionResumeVerdict, SessionError> {
match self.prepare_committed_boundary_resume(session_id).await? {
meerkat_session::PreparedCommittedBoundaryResume::Materializable {
session,
materialization,
observation,
preparation,
} => {
let authority = SessionResumeAuthority {
observation: Some(observation),
};
let load = match materialization {
meerkat_session::PreparedCommittedBoundaryResumeMaterialization::Active => {
ResumeSessionLoad::Active(session)
}
meerkat_session::PreparedCommittedBoundaryResumeMaterialization::Revivable => {
ResumeSessionLoad::Revivable(session)
}
};
let preparation = SessionResumePreparationReceipt::from_persistent(
session_id,
&authority,
preparation,
);
SessionResumeVerdict::from_authoritative_load_with_authority(
session_id,
load,
authority,
Some(preparation),
)
}
meerkat_session::PreparedCommittedBoundaryResume::Unavailable {
unavailable,
observation,
} => {
let authority = SessionResumeAuthority {
observation: Some(observation),
};
let load = match unavailable {
meerkat_session::PreparedCommittedBoundaryResumeUnavailable::Absent => {
ResumeSessionLoad::Absent
}
meerkat_session::PreparedCommittedBoundaryResumeUnavailable::ArchivedNotRevivable {
runtime_state,
} => ResumeSessionLoad::ArchivedNotRevivable { runtime_state },
};
SessionResumeVerdict::from_authoritative_load_with_authority(
session_id,
load,
authority,
None,
)
}
meerkat_session::PreparedCommittedBoundaryResume::CommittedBoundaryUnprovable {
observation,
reason,
} => SessionResumeVerdict::committed_boundary_unprovable(
session_id,
ResumeSessionLoad::Absent,
SessionResumeAuthority {
observation: Some(observation),
},
reason,
),
meerkat_session::PreparedCommittedBoundaryResume::AuthorityChangedDuringMaterialization {
observation,
} => Ok(SessionResumeVerdict::authority_changed_during_materialization(
session_id,
SessionResumeAuthority {
observation: Some(observation),
},
)),
}
}
async fn create_session_with_actor_witness_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
resume_preparation: Option<SessionResumePreparationReceipt>,
actor_witness_slot: &meerkat_session::LiveSessionActorWitnessSlot,
) -> Result<meerkat_core::RunResult, SessionError> {
let resume_preparation = match resume_preparation {
Some(preparation) => {
let session_id = req
.build
.as_ref()
.and_then(|build| build.resume_session.as_ref())
.map(|session| session.id())
.ok_or_else(|| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(
"persistent actor materialization carried a resume preparation receipt without its session body"
.to_string(),
))
})?;
Some(preparation.into_persistent_for(session_id)?)
}
None => None,
};
let admission = self.reserve_create_session_admission().await?;
self.create_session_with_reserved_admission_and_actor_witness_under_runtime_turn_boundary(
req,
admission,
resume_preparation,
actor_witness_slot,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn create_session_with_machine_archived_resume_authority(
&self,
req: meerkat_core::service::CreateSessionRequest,
authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
) -> Result<meerkat_core::RunResult, SessionError> {
let admission = self.reserve_create_session_admission().await?;
self.create_session_with_reserved_machine_archived_resume_admission(
req,
admission,
authorization,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn create_session_with_machine_archived_resume_authority_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
) -> Result<meerkat_core::RunResult, SessionError> {
let admission = self.reserve_create_session_admission().await?;
self.create_session_with_reserved_machine_archived_resume_admission_under_runtime_turn_boundary(
req,
admission,
authorization,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn create_session_with_machine_archived_resume_authority_and_actor_witness_under_runtime_turn_boundary(
&self,
req: meerkat_core::service::CreateSessionRequest,
authorization: meerkat_runtime::ArchivedSessionActorMaterializationAuthorization,
resume_preparation: SessionResumePreparationReceipt,
actor_witness_slot: &meerkat_session::LiveSessionActorWitnessSlot,
) -> Result<meerkat_core::RunResult, SessionError> {
let session_id = req
.build
.as_ref()
.and_then(|build| build.resume_session.as_ref())
.map(|session| session.id())
.ok_or_else(|| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(
"machine-authorized archived resume omitted its session body".to_string(),
))
})?;
let resume_preparation = resume_preparation.into_persistent_for(session_id)?;
let admission = self.reserve_create_session_admission().await?;
self.create_session_with_reserved_machine_archived_resume_admission_and_actor_witness_under_runtime_turn_boundary(
req,
admission,
authorization,
resume_preparation,
actor_witness_slot,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn authorize_revivable_retired_session(
&self,
session_id: &SessionId,
authority: meerkat_runtime::PreparedArchivedResumeCommitLease,
) -> Result<meerkat_runtime::AuthorizedArchivedResumeCommitLease, SessionError> {
self.revive_archived_session_with_prepared_materialization(session_id, authority)
.await
}
fn supports_persistent_sessions(&self) -> bool {
true
}
fn persisted_session_authority_read_cost(&self) -> PersistedSessionAuthorityReadCost {
match self.runtime_store().session_boundary_authority_read_cost() {
meerkat_runtime::store::RuntimeSessionAuthorityReadCost::Bounded => {
PersistedSessionAuthorityReadCost::Bounded
}
meerkat_runtime::store::RuntimeSessionAuthorityReadCost::Unsupported => {
PersistedSessionAuthorityReadCost::Unsupported
}
}
}
async fn observe_persisted_session_authority(
&self,
session_id: &SessionId,
) -> Result<Option<crate::identity::IdentitySessionStoreAuthority>, SessionError> {
let authority =
meerkat_session::PersistentSessionService::<B>::observe_persisted_session_authority(
self, session_id,
)
.await?;
Ok(authority.map(crate::identity::IdentitySessionStoreAuthority::from_runtime_authority))
}
async fn observe_session_resume_authority(
&self,
session_id: &SessionId,
) -> Result<SessionResumeAuthority, SessionError> {
let runtime_id = meerkat_runtime::LogicalRuntimeId::for_session(session_id);
let observation = self
.runtime_store()
.load_session_resume_observation(&runtime_id)
.await
.map_err(|error| {
SessionError::Agent(meerkat_core::error::AgentError::InternalError(format!(
"failed to atomically observe resume authority for session '{session_id}': {error}"
)))
})?;
Ok(SessionResumeAuthority {
observation: Some(observation),
})
}
async fn live_session_actor_registered(
&self,
session_id: &SessionId,
) -> Result<bool, SessionError> {
Ok(
meerkat_session::PersistentSessionService::<B>::live_session_actor_registered(
self, session_id,
)
.await,
)
}
#[cfg(feature = "runtime-adapter")]
fn runtime_adapter(&self) -> Option<Arc<meerkat_runtime::MeerkatMachine>> {
#[cfg(target_arch = "wasm32")]
{
None
}
#[cfg(not(target_arch = "wasm32"))]
{
let key = std::ptr::from_ref(self) as usize;
let store = self.runtime_store();
Some(cached_runtime_adapter(
persistent_runtime_adapter_cache(),
key,
|| {
Arc::new(meerkat_runtime::MeerkatMachine::persistent(
store,
self.blob_store(),
))
},
))
}
}
#[cfg(feature = "runtime-adapter")]
fn supports_runtime_turn_apply(&self) -> bool {
true
}
async fn load_persisted_session(
&self,
session_id: &SessionId,
) -> Result<Option<Session>, SessionError> {
let Some(session) = self.load_authoritative_session(session_id).await? else {
return Ok(None);
};
if self
.session_archived_by_authority(session_id, &session)
.await?
{
tracing::debug!(
session_id = %session_id,
"load_persisted_session hid an archived session (use the resume seam for revival)"
);
return Ok(None);
}
Ok(Some(session))
}
async fn fork_persisted_session(
&self,
source_session_id: &SessionId,
message_count: Option<usize>,
tool_access_policy: Option<meerkat_core::ops::ToolAccessPolicy>,
target: meerkat_core::DurableSessionForkTarget,
) -> Result<meerkat_core::SessionForkResult, SessionError> {
meerkat_session::PersistentSessionService::<B>::fork_durable_session(
self,
source_session_id,
message_count,
tool_access_policy,
Some(target),
)
.await
}
async fn load_revivable_retired_session(
&self,
session_id: &SessionId,
) -> Result<Option<Session>, SessionError> {
match self.load_session_for_resume(session_id).await? {
ResumeSessionLoad::Revivable(session) => Ok(Some(*session)),
ResumeSessionLoad::Active(_)
| ResumeSessionLoad::ArchivedNotRevivable { .. }
| ResumeSessionLoad::Absent => Ok(None),
}
}
async fn load_session_for_resume(
&self,
session_id: &SessionId,
) -> Result<ResumeSessionLoad, SessionError> {
let Some(session) = self.observe_authoritative_session_body(session_id).await? else {
return Ok(ResumeSessionLoad::Absent);
};
let runtime_state = self.persisted_runtime_state(session_id).await?;
let store_archived = self
.session_archived_by_authority(session_id, &session)
.await?;
let imported_document_archived = runtime_state.is_none()
&& session
.lifecycle_terminal()
.is_some_and(meerkat_core::SessionLifecycleTerminal::is_archived);
let archived = store_archived || imported_document_archived;
if !archived {
if runtime_state == Some(meerkat_runtime::RuntimeState::Retired) {
return Ok(ResumeSessionLoad::Revivable(Box::new(session)));
}
return Ok(ResumeSessionLoad::Active(Box::new(session)));
}
match runtime_state {
Some(meerkat_runtime::RuntimeState::Retired | meerkat_runtime::RuntimeState::Idle) => {
Ok(ResumeSessionLoad::Revivable(Box::new(session)))
}
None => Ok(ResumeSessionLoad::Revivable(Box::new(session))),
other => Ok(ResumeSessionLoad::ArchivedNotRevivable {
runtime_state: other,
}),
}
}
async fn load_persisted_session_metadata(
&self,
session_id: &SessionId,
) -> Result<Option<meerkat_core::PersistedSessionMetadataView>, SessionError> {
let Some(view) = self.load_authoritative_session_metadata(session_id).await? else {
return Ok(None);
};
if self
.session_archived_by_authority_with_terminal(
session_id,
view.lifecycle_terminal.as_ref(),
)
.await?
{
return Ok(None);
}
Ok(Some(view))
}
async fn session_known_to_archive_authority(
&self,
session_id: &SessionId,
) -> Result<bool, SessionError> {
meerkat_session::PersistentSessionService::<B>::session_known_to_archive_authority(
self, session_id,
)
.await
}
async fn archive_with_mob_lifecycle_authority(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
#[cfg(feature = "runtime-adapter")]
if let Some(runtime_adapter) = self.runtime_adapter() {
return meerkat_session::PersistentSessionService::<B>::archive_with_machine_protocol(
self,
session_id,
meerkat_session::MachineSessionArchiveProtocol::from_machine(
runtime_adapter.as_ref(),
),
)
.await;
}
<Self as SessionService>::archive(self, session_id).await
}
#[cfg(feature = "runtime-adapter")]
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary_and_hook_before(
&self,
session_id: &SessionId,
deadline: meerkat_core::time_compat::Instant,
post_commit_hook: Option<Arc<dyn meerkat_runtime::MachineSessionArchivePostCommitHook>>,
) -> Result<(), SessionError> {
if let Some(runtime_adapter) = self.runtime_adapter() {
return meerkat_session::PersistentSessionService::<B>::archive_with_machine_protocol_under_runtime_turn_boundary_and_hook_before(
self,
session_id,
meerkat_session::MachineSessionArchiveProtocol::from_machine(
runtime_adapter.as_ref(),
),
deadline,
post_commit_hook,
)
.await;
}
if post_commit_hook.is_some() {
return Err(SessionError::Unsupported(format!(
"non-runtime persistent session {session_id} cannot run a pre-retire archive hook"
)));
}
<Self as SessionService>::archive(self, session_id).await
}
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
#[cfg(feature = "runtime-adapter")]
if let Some(runtime_adapter) = self.runtime_adapter() {
return meerkat_session::PersistentSessionService::<B>::archive_with_machine_protocol_under_runtime_turn_boundary(
self,
session_id,
meerkat_session::MachineSessionArchiveProtocol::from_machine(
runtime_adapter.as_ref(),
),
)
.await;
}
<Self as SessionService>::archive(self, session_id).await
}
async fn archive_with_mob_lifecycle_authority_under_runtime_turn_boundary_before(
&self,
session_id: &SessionId,
deadline: meerkat_core::time_compat::Instant,
) -> Result<(), SessionError> {
#[cfg(feature = "runtime-adapter")]
if let Some(runtime_adapter) = self.runtime_adapter() {
return meerkat_session::PersistentSessionService::<B>::archive_with_machine_protocol_under_runtime_turn_boundary_before(
self,
session_id,
meerkat_session::MachineSessionArchiveProtocol::from_machine(
runtime_adapter.as_ref(),
),
deadline,
)
.await;
}
<Self as SessionService>::archive(self, session_id).await
}
#[cfg(feature = "runtime-adapter")]
async fn interrupt_with_machine_authority(
&self,
session_id: &SessionId,
authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::interrupt_with_machine_authority(
self, session_id, authority,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn interrupt_run_with_machine_authority(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<bool, SessionError> {
meerkat_session::PersistentSessionService::<B>::interrupt_run_with_machine_authority(
self,
session_id,
expected_run_id,
authority,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn cancel_after_boundary_with_machine_authority(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::cancel_after_boundary_with_machine_authority(
self, session_id, expected_run_id, authority,
)
.await
}
#[cfg(feature = "runtime-adapter")]
async fn cancel_current_after_boundary_with_machine_authority(
&self,
session_id: &SessionId,
authority: meerkat_runtime::MachineSessionControlAuthority,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::cancel_current_after_boundary_with_machine_authority(
self, session_id, authority,
)
.await
}
async fn execution_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<AgentExecutionSnapshot>, SessionError> {
meerkat_session::PersistentSessionService::<B>::execution_snapshot(self, session_id).await
}
async fn tool_scope_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<ToolScopeSnapshot>, SessionError> {
meerkat_session::PersistentSessionService::<B>::tool_scope_snapshot(self, session_id).await
}
async fn external_tool_surface_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<ExternalToolSurfaceSnapshot>, SessionError> {
meerkat_session::PersistentSessionService::<B>::external_tool_surface_snapshot(
self, session_id,
)
.await
}
async fn peer_ingress_runtime_snapshot(
&self,
session_id: &SessionId,
) -> Result<Option<PeerIngressRuntimeSnapshot>, SessionError> {
let Some(runtime) = self.comms_runtime(session_id).await else {
return Ok(None);
};
match runtime.peer_ingress_runtime_snapshot().await {
Ok(snapshot) => Ok(Some(snapshot)),
Err(CommsCapabilityError::Unsupported(_)) => Ok(None),
Err(error) => Err(SessionError::Unsupported(error.to_string())),
}
}
async fn subscribe_session_events(
&self,
session_id: &SessionId,
) -> Result<EventStream, StreamError> {
meerkat_session::PersistentSessionService::<B>::subscribe_session_events(self, session_id)
.await
}
async fn discard_live_session(&self, session_id: &SessionId) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::discard_live_session(self, session_id).await
}
async fn await_event_projection_drain(
&self,
session_id: &SessionId,
) -> Result<bool, SessionError> {
meerkat_session::PersistentSessionService::<B>::event_log_await_projection_drain(
self, session_id,
)
.await
}
async fn apply_runtime_turn(
&self,
session_id: &SessionId,
run_id: RunId,
req: StartTurnRequest,
boundary: RunApplyBoundary,
contributing_input_ids: Vec<InputId>,
) -> Result<CoreApplyOutput, SessionError> {
meerkat_session::PersistentSessionService::<B>::apply_runtime_turn(
self,
session_id,
run_id,
req,
boundary,
contributing_input_ids,
)
.await
}
async fn prepare_transient_turn_context_for_active_turn(
&self,
session_id: &SessionId,
expected_run_id: &RunId,
contexts: Vec<TurnRequestContext>,
) -> Result<meerkat_core::CoreBoundaryStageOutput, meerkat_core::CoreBoundaryStageError> {
meerkat_session::PersistentSessionService::<B>::prepare_live_transient_turn_context_boundary(
self,
session_id,
expected_run_id,
contexts,
)
.await
}
async fn checkpoint_committed_runtime_session_snapshot(
&self,
session_id: &SessionId,
session_snapshot: Arc<Vec<u8>>,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::checkpoint_committed_runtime_session_snapshot(
self,
session_id,
session_snapshot,
)
.await
}
async fn acquire_runtime_turn_finalization_guard(
&self,
session_id: &SessionId,
) -> Result<Box<dyn meerkat_core::lifecycle::CoreExecutorTurnFinalizationGuard>, SessionError>
{
Ok(Box::new(
meerkat_session::PersistentSessionService::<B>::acquire_runtime_turn_finalization_guard(
self,
session_id,
)
.await,
))
}
async fn checkpoint_committed_runtime_session_snapshot_under_turn_finalization_boundary(
&self,
session_id: &SessionId,
session_snapshot: Arc<Vec<u8>>,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::checkpoint_committed_runtime_session_snapshot_under_runtime_turn_boundary(
self,
session_id,
session_snapshot,
)
.await
}
async fn acknowledge_committed_runtime_session_boundary_under_turn_finalization_boundary(
&self,
session_id: &SessionId,
authority: &meerkat_core::CommittedSessionBoundaryAuthority,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::acknowledge_committed_runtime_session_boundary_under_runtime_turn_boundary(
self,
session_id,
authority,
)
.await
}
async fn discard_live_session_after_runtime_stop_terminalized(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::discard_live_session_after_runtime_stop_terminalized(
self,
session_id,
)
.await
}
async fn discard_live_session_after_runtime_stop_terminalized_under_turn_finalization_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::discard_live_session_after_runtime_stop_terminalized_under_runtime_turn_boundary(
self,
session_id,
)
.await
}
async fn discard_live_session_under_runtime_turn_boundary(
&self,
session_id: &SessionId,
) -> Result<(), SessionError> {
meerkat_session::PersistentSessionService::<B>::discard_live_session_under_runtime_turn_boundary(
self,
session_id,
)
.await
}
async fn discard_live_session_actor_under_runtime_turn_boundary(
&self,
witness: &meerkat_session::LiveSessionActorWitness,
) -> Result<bool, SessionError> {
meerkat_session::PersistentSessionService::<B>::discard_live_session_actor_under_runtime_turn_boundary(
self, witness,
)
.await
}
async fn discard_live_session_actor_after_durability_reload_required(
&self,
witness: &meerkat_session::LiveSessionActorWitness,
) -> Result<bool, SessionError> {
meerkat_session::PersistentSessionService::<B>::discard_live_session_actor_after_durability_reload_required(
self, witness,
)
.await
}
async fn publish_interaction_terminals_for_actor(
&self,
actor_witness: &meerkat_session::LiveSessionActorWitness,
events: &[meerkat_core::event::AgentEvent],
) -> Result<
Vec<meerkat_core::lifecycle::core_executor::CoreInteractionTerminalPublicationReceipt>,
SessionError,
> {
meerkat_session::PersistentSessionService::<B>::publish_interaction_terminals_exact_batch_for_actor(
self,
actor_witness,
events,
)
.await
}
async fn cancel_all_checkpointers(&self) {
meerkat_session::PersistentSessionService::<B>::cancel_all_checkpointers(self).await;
}
async fn rearm_all_checkpointers(&self) {
meerkat_session::PersistentSessionService::<B>::rearm_all_checkpointers(self).await;
}
}