use meerkat_core::types::{ContentInput, SessionId};
use meerkat_core::{
AppendSystemContextRequest, AppendSystemContextStatus, Session, SessionLlmIdentity,
SessionSystemContextState,
};
use meerkat_runtime::meerkat_machine::dsl as machine_dsl;
use std::collections::{BTreeMap, HashMap};
#[cfg(not(target_arch = "wasm32"))]
use tokio::sync::RwLock;
#[cfg(target_arch = "wasm32")]
use tokio_with_wasm::alias::sync::RwLock;
use crate::AgentBuildConfig;
enum StagedPayload {
Staged {
build_config: Box<AgentBuildConfig>,
},
Closing {
build_config: Box<AgentBuildConfig>,
},
Promoting {
starting_system_context_state: SessionSystemContextState,
current_system_context_state: SessionSystemContextState,
},
}
struct GeneratedStagedSessionAuthority {
authority: machine_dsl::MeerkatMachineAuthority,
}
impl GeneratedStagedSessionAuthority {
fn new(
id: &SessionId,
keep_alive: bool,
has_comms_name: bool,
effective_llm_identity: &SessionLlmIdentity,
machine_archived_resume_authorized: bool,
) -> Result<Self, StagedLifecycleError> {
let mut this = Self {
authority: machine_dsl::MeerkatMachineAuthority::new(),
};
Self::map_keep_alive_rejection(
this.apply(
machine_dsl::MeerkatMachineInput::StageDeferredSession {
session_id: machine_dsl::SessionId::from_domain(id),
keep_alive,
has_comms_name,
llm_identity: machine_dsl::SessionLlmIdentity::from_domain(
effective_llm_identity,
),
machine_archived_resume_authorized,
},
"StageDeferredSession",
),
keep_alive,
has_comms_name,
)?;
Ok(this)
}
fn apply(
&mut self,
input: machine_dsl::MeerkatMachineInput,
context: &'static str,
) -> Result<(), StagedLifecycleError> {
machine_dsl::MeerkatMachineMutator::apply(&mut self.authority, input)
.map(|_| ())
.map_err(|err| {
StagedLifecycleError::GeneratedAuthorityRejected {
context,
reason: match err {
machine_dsl::MeerkatMachineTransitionError::NoMatchingTransition {
phase,
trigger,
} => {
format!("no matching transition from {phase:?} for {trigger}")
}
machine_dsl::MeerkatMachineTransitionError::GuardRejected {
phase,
trigger,
} => {
format!("guard rejected transition from {phase:?} for {trigger}")
}
machine_dsl::MeerkatMachineTransitionError::RecoveredStateInvariantRejected {
phase,
invariant,
} => {
format!("recovered state violated invariant {invariant} in phase {phase:?}")
}
},
}
})
}
fn phase(&self) -> machine_dsl::StagedSessionPhase {
self.authority.state().staged_session_phase
}
fn keep_alive(&self) -> Option<bool> {
self.authority.state().staged_session_keep_alive
}
fn machine_archived_resume_authorized(&self) -> bool {
self.authority
.state()
.staged_session_machine_archived_resume_authorized
}
fn effective_llm_identity(
&self,
context: &'static str,
) -> Result<SessionLlmIdentity, StagedLifecycleError> {
let identity = self
.authority
.state()
.staged_session_llm_identity
.clone()
.ok_or_else(|| StagedLifecycleError::GeneratedIdentityUnavailable {
context,
reason: "generated staged-session authority has no LLM identity".to_string(),
})?;
SessionLlmIdentity::try_from(identity).map_err(|reason| {
StagedLifecycleError::GeneratedIdentityUnavailable { context, reason }
})
}
fn update_keep_alive(
&mut self,
id: &SessionId,
keep_alive: bool,
has_comms_name: bool,
) -> Result<(), StagedLifecycleError> {
Self::map_keep_alive_rejection(
self.apply(
machine_dsl::MeerkatMachineInput::UpdateDeferredSessionKeepAlive {
session_id: machine_dsl::SessionId::from_domain(id),
keep_alive,
has_comms_name,
},
"UpdateDeferredSessionKeepAlive",
),
keep_alive,
has_comms_name,
)
}
fn update_llm_identity(
&mut self,
id: &SessionId,
effective_llm_identity: &SessionLlmIdentity,
) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::UpdateDeferredSessionLlmIdentity {
session_id: machine_dsl::SessionId::from_domain(id),
llm_identity: machine_dsl::SessionLlmIdentity::from_domain(effective_llm_identity),
},
"UpdateDeferredSessionLlmIdentity",
)
}
fn authorize_system_context_append(
&mut self,
id: &SessionId,
) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::AuthorizeDeferredSessionSystemContextAppend {
session_id: machine_dsl::SessionId::from_domain(id),
},
"AuthorizeDeferredSessionSystemContextAppend",
)
}
fn map_keep_alive_rejection(
result: Result<(), StagedLifecycleError>,
keep_alive: bool,
has_comms_name: bool,
) -> Result<(), StagedLifecycleError> {
match result {
Err(StagedLifecycleError::GeneratedAuthorityRejected { .. })
if keep_alive && !has_comms_name =>
{
Err(StagedLifecycleError::KeepAliveRequiresCommsName)
}
other => other,
}
}
fn begin_promotion(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::BeginDeferredSessionPromotion {
session_id: machine_dsl::SessionId::from_domain(id),
},
"BeginDeferredSessionPromotion",
)
}
fn abandon_promotion(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::AbandonDeferredSessionPromotion {
session_id: machine_dsl::SessionId::from_domain(id),
},
"AbandonDeferredSessionPromotion",
)
}
fn authorize_machine_archived_resume(
&mut self,
id: &SessionId,
) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::AuthorizeDeferredSessionMachineArchivedResume {
session_id: machine_dsl::SessionId::from_domain(id),
},
"AuthorizeDeferredSessionMachineArchivedResume",
)
}
fn finish_promotion(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::FinishDeferredSessionPromotion {
session_id: machine_dsl::SessionId::from_domain(id),
},
"FinishDeferredSessionPromotion",
)
}
fn begin_archive(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::BeginDeferredSessionArchive {
session_id: machine_dsl::SessionId::from_domain(id),
},
"BeginDeferredSessionArchive",
)
}
fn restore_archive(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::RestoreDeferredSessionArchive {
session_id: machine_dsl::SessionId::from_domain(id),
},
"RestoreDeferredSessionArchive",
)
}
fn finish_archive(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::FinishDeferredSessionArchive {
session_id: machine_dsl::SessionId::from_domain(id),
},
"FinishDeferredSessionArchive",
)
}
fn drop_session(&mut self, id: &SessionId) -> Result<(), StagedLifecycleError> {
self.apply(
machine_dsl::MeerkatMachineInput::DropDeferredSession {
session_id: machine_dsl::SessionId::from_domain(id),
},
"DropDeferredSession",
)
}
}
pub struct StagedSlot {
authority: GeneratedStagedSessionAuthority,
payload: StagedPayload,
pub labels: Option<BTreeMap<String, String>>,
pub deferred_prompt: Option<ContentInput>,
pub created_at_secs: u64,
pub updated_at_secs: u64,
}
impl StagedSlot {
#[allow(clippy::too_many_arguments)]
pub fn new_staged(
id: &SessionId,
build_config: AgentBuildConfig,
effective_llm_identity: SessionLlmIdentity,
labels: Option<BTreeMap<String, String>>,
deferred_prompt: Option<ContentInput>,
created_at_secs: u64,
updated_at_secs: u64,
machine_archived_resume_authorized: bool,
) -> Result<Self, StagedLifecycleError> {
let keep_alive = build_config.keep_alive;
let has_comms_name = build_config.comms_name.is_some();
Ok(Self {
authority: GeneratedStagedSessionAuthority::new(
id,
keep_alive,
has_comms_name,
&effective_llm_identity,
machine_archived_resume_authorized,
)?,
payload: StagedPayload::Staged {
build_config: Box::new(build_config),
},
labels,
deferred_prompt,
created_at_secs,
updated_at_secs,
})
}
}
#[derive(Clone)]
pub struct StagedSessionInfo {
pub labels: BTreeMap<String, String>,
pub effective_llm_identity: SessionLlmIdentity,
pub created_at_secs: u64,
pub updated_at_secs: u64,
pub is_promoting: bool,
}
pub struct PromotingSlot {
pub build_config: Box<AgentBuildConfig>,
pub effective_llm_identity: SessionLlmIdentity,
pub labels: Option<BTreeMap<String, String>>,
pub deferred_prompt: Option<ContentInput>,
pub created_at_secs: u64,
pub updated_at_secs: u64,
pub generated_machine_archived_resume_admission: GeneratedMachineArchivedResumeAdmission,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GeneratedMachineArchivedResumeAdmission {
authorized_by_generated_authority: bool,
}
impl GeneratedMachineArchivedResumeAdmission {
fn from_generated_authority(authority: &GeneratedStagedSessionAuthority) -> Self {
Self {
authorized_by_generated_authority: authority.machine_archived_resume_authorized(),
}
}
pub fn is_authorized_by_generated_authority(self) -> bool {
self.authorized_by_generated_authority
}
}
#[derive(Debug, thiserror::Error)]
pub enum StagedLifecycleError {
#[error("session already staged: {0}")]
AlreadyStaged(SessionId),
#[error("session already being promoted: {0}")]
AlreadyPromoting(SessionId),
#[error("staged session not found: {0}")]
NotFound(SessionId),
#[error("keep_alive requires a session created with comms_name")]
KeepAliveRequiresCommsName,
#[error("generated staged-session authority rejected {context}: {reason}")]
GeneratedAuthorityRejected {
context: &'static str,
reason: String,
},
#[error("generated staged-session identity unavailable during {context}: {reason}")]
GeneratedIdentityUnavailable {
context: &'static str,
reason: String,
},
}
#[derive(Default)]
pub struct StagedSessionRegistry {
slots: RwLock<HashMap<SessionId, StagedSlot>>,
}
impl StagedSessionRegistry {
pub fn new() -> Self {
Self::default()
}
pub async fn len(&self) -> usize {
self.slots.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.slots.read().await.is_empty()
}
pub async fn contains(&self, id: &SessionId) -> bool {
self.slots.read().await.contains_key(id)
}
pub async fn info(
&self,
id: &SessionId,
) -> Result<Option<StagedSessionInfo>, StagedLifecycleError> {
let slots = self.slots.read().await;
slots.get(id).map(Self::slot_info).transpose()
}
pub async fn project_info(&self, id: &SessionId) -> Option<StagedSessionInfo> {
match self.info(id).await {
Ok(info) => info,
Err(error) => {
tracing::warn!(
session_id = %id,
error = %error,
"failed to project staged-session info from generated authority"
);
None
}
}
}
pub async fn try_info(
&self,
id: &SessionId,
) -> Result<Option<StagedSessionInfo>, StagedLifecycleError> {
self.info(id).await
}
pub async fn effective_llm_identity(
&self,
id: &SessionId,
) -> Result<Option<SessionLlmIdentity>, StagedLifecycleError> {
let slots = self.slots.read().await;
slots
.get(id)
.map(|s| s.authority.effective_llm_identity("effective_llm_identity"))
.transpose()
}
pub async fn keep_alive(&self, id: &SessionId) -> Option<bool> {
let slots = self.slots.read().await;
let slot = slots.get(id)?;
slot.authority.keep_alive()
}
pub async fn update_keep_alive(
&self,
id: &SessionId,
keep_alive: bool,
updated_at_secs: u64,
) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(false);
};
match &mut slot.payload {
StagedPayload::Staged { build_config } => {
slot.authority.update_keep_alive(
id,
keep_alive,
build_config.comms_name.is_some(),
)?;
build_config.keep_alive = keep_alive;
slot.updated_at_secs = updated_at_secs;
Ok(true)
}
StagedPayload::Promoting { .. } | StagedPayload::Closing { .. } => {
Err(StagedLifecycleError::AlreadyPromoting(id.clone()))
}
}
}
pub async fn authorize_promoting_keep_alive(
&self,
id: &SessionId,
keep_alive: bool,
has_comms_name: bool,
) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(false);
};
match &mut slot.payload {
StagedPayload::Promoting { .. } => {
slot.authority
.update_keep_alive(id, keep_alive, has_comms_name)?;
Ok(true)
}
StagedPayload::Staged { .. } => Err(StagedLifecycleError::GeneratedAuthorityRejected {
context: "UpdateDeferredSessionKeepAlive",
reason: "payload is not in generated promoting phase".to_string(),
}),
StagedPayload::Closing { .. } => {
Err(StagedLifecycleError::AlreadyPromoting(id.clone()))
}
}
}
pub async fn update_effective_llm_identity(
&self,
id: &SessionId,
effective_llm_identity: &SessionLlmIdentity,
) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(false);
};
match &mut slot.payload {
StagedPayload::Staged { .. } | StagedPayload::Promoting { .. } => {
slot.authority
.update_llm_identity(id, effective_llm_identity)?;
Ok(true)
}
StagedPayload::Closing { .. } => {
Err(StagedLifecycleError::AlreadyPromoting(id.clone()))
}
}
}
pub async fn authorize_machine_archived_resume(
&self,
id: &SessionId,
) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(false);
};
if !matches!(slot.payload, StagedPayload::Promoting { .. }) {
return Err(StagedLifecycleError::AlreadyPromoting(id.clone()));
}
slot.authority.authorize_machine_archived_resume(id)?;
Ok(true)
}
pub async fn list(
&self,
label_filter: Option<&BTreeMap<String, String>>,
) -> Vec<(SessionId, StagedSessionInfo)> {
let slots = self.slots.read().await;
slots
.iter()
.filter(|(_, slot)| Self::matches_label_filter(slot.labels.as_ref(), label_filter))
.filter_map(|(id, slot)| match Self::slot_info(slot) {
Ok(info) => Some((id.clone(), info)),
Err(error) => {
tracing::warn!(
session_id = %id,
error = %error,
"skipping staged-session projection without generated identity"
);
None
}
})
.collect()
}
pub async fn stage(&self, id: SessionId, slot: StagedSlot) -> Result<(), StagedLifecycleError> {
let mut slots = self.slots.write().await;
if slots.contains_key(&id) {
return Err(StagedLifecycleError::AlreadyStaged(id));
}
slots.insert(id, slot);
Ok(())
}
pub async fn begin_promotion(
&self,
id: &SessionId,
) -> Result<Option<PromotingSlot>, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(None);
};
let starting_state = match &slot.payload {
StagedPayload::Staged { build_config } => build_config
.resume_session
.as_ref()
.and_then(Session::system_context_state)
.unwrap_or_default(),
StagedPayload::Promoting { .. } | StagedPayload::Closing { .. } => {
return Err(StagedLifecycleError::AlreadyPromoting(id.clone()));
}
};
slot.authority.begin_promotion(id)?;
let payload = std::mem::replace(
&mut slot.payload,
StagedPayload::Promoting {
starting_system_context_state: starting_state.clone(),
current_system_context_state: starting_state,
},
);
let StagedPayload::Staged { build_config } = payload else {
return Err(StagedLifecycleError::GeneratedAuthorityRejected {
context: "BeginDeferredSessionPromotion",
reason: "payload shape diverged from generated staged phase".to_string(),
});
};
let effective_llm_identity = slot
.authority
.effective_llm_identity("BeginDeferredSessionPromotion")?;
Ok(Some(PromotingSlot {
build_config,
effective_llm_identity,
labels: slot.labels.clone(),
deferred_prompt: slot.deferred_prompt.clone(),
created_at_secs: slot.created_at_secs,
updated_at_secs: slot.updated_at_secs,
generated_machine_archived_resume_admission:
GeneratedMachineArchivedResumeAdmission::from_generated_authority(&slot.authority),
}))
}
pub async fn take_promoting_system_context_state(
&self,
id: &SessionId,
) -> Option<(SessionSystemContextState, SessionSystemContextState)> {
let mut slots = self.slots.write().await;
let should_remove = {
let slot = slots.get_mut(id)?;
if slot.authority.finish_promotion(id).is_err() {
return None;
}
true
};
if !should_remove {
return None;
}
let slot = slots.remove(id)?;
match slot.payload {
StagedPayload::Promoting {
starting_system_context_state,
current_system_context_state,
} => Some((starting_system_context_state, current_system_context_state)),
StagedPayload::Staged { .. } | StagedPayload::Closing { .. } => None,
}
}
pub async fn promoting_system_context_state(
&self,
id: &SessionId,
) -> Option<(SessionSystemContextState, SessionSystemContextState)> {
let slots = self.slots.read().await;
let slot = slots.get(id)?;
match &slot.payload {
StagedPayload::Promoting {
starting_system_context_state,
current_system_context_state,
} => Some((
starting_system_context_state.clone(),
current_system_context_state.clone(),
)),
StagedPayload::Staged { .. } | StagedPayload::Closing { .. } => None,
}
}
#[allow(clippy::too_many_arguments)]
pub async fn abandon_promotion(
&self,
id: SessionId,
build_config: AgentBuildConfig,
labels: Option<BTreeMap<String, String>>,
deferred_prompt: Option<ContentInput>,
created_at_secs: u64,
updated_at_secs: u64,
) -> bool {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(&id) else {
return false;
};
if !matches!(slot.payload, StagedPayload::Promoting { .. }) {
return false;
}
if slot.authority.abandon_promotion(&id).is_err() {
return false;
}
let updated_at_secs = updated_at_secs.max(slot.updated_at_secs);
slot.payload = StagedPayload::Staged {
build_config: Box::new(build_config),
};
slot.labels = labels;
slot.deferred_prompt = deferred_prompt;
slot.created_at_secs = created_at_secs;
slot.updated_at_secs = updated_at_secs;
true
}
pub async fn begin_archive(&self, id: &SessionId) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(false);
};
match &mut slot.payload {
StagedPayload::Staged { .. } => {
slot.authority.begin_archive(id)?;
let payload = std::mem::replace(
&mut slot.payload,
StagedPayload::Promoting {
starting_system_context_state: SessionSystemContextState::default(),
current_system_context_state: SessionSystemContextState::default(),
},
);
let StagedPayload::Staged { build_config } = payload else {
return Err(StagedLifecycleError::GeneratedAuthorityRejected {
context: "BeginDeferredSessionArchive",
reason: "payload shape diverged from generated archive phase".to_string(),
});
};
slot.payload = StagedPayload::Closing { build_config };
Ok(true)
}
StagedPayload::Promoting { .. } | StagedPayload::Closing { .. } => {
Err(StagedLifecycleError::AlreadyPromoting(id.clone()))
}
}
}
pub async fn restore_archive(&self, id: &SessionId) -> bool {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return false;
};
if !matches!(slot.payload, StagedPayload::Closing { .. }) {
return false;
}
if slot.authority.restore_archive(id).is_err() {
return false;
}
let payload = std::mem::replace(
&mut slot.payload,
StagedPayload::Promoting {
starting_system_context_state: SessionSystemContextState::default(),
current_system_context_state: SessionSystemContextState::default(),
},
);
let StagedPayload::Closing { build_config } = payload else {
return false;
};
slot.payload = StagedPayload::Staged { build_config };
true
}
pub async fn finish_archive(&self, id: &SessionId) -> bool {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return false;
};
if !matches!(slot.payload, StagedPayload::Closing { .. }) {
return false;
}
if slot.authority.finish_archive(id).is_err() {
return false;
}
slots.remove(id);
true
}
pub async fn abandon(&self, id: &SessionId) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(slot) = slots.get_mut(id) else {
return Ok(false);
};
slot.authority.drop_session(id)?;
Ok(slots.remove(id).is_some())
}
pub async fn abandon_staged(&self, id: &SessionId) -> Result<bool, StagedLifecycleError> {
let mut slots = self.slots.write().await;
let Some(phase) = slots.get(id).map(|slot| slot.authority.phase()) else {
return Ok(false);
};
match phase {
machine_dsl::StagedSessionPhase::Staged => {
let slot = slots
.get_mut(id)
.ok_or_else(|| StagedLifecycleError::NotFound(id.clone()))?;
slot.authority.drop_session(id)?;
slots.remove(id);
Ok(true)
}
machine_dsl::StagedSessionPhase::Promoting
| machine_dsl::StagedSessionPhase::Closing => {
Err(StagedLifecycleError::AlreadyPromoting(id.clone()))
}
machine_dsl::StagedSessionPhase::NotStaged => Ok(false),
}
}
pub async fn clear(&self) {
let mut slots = self.slots.write().await;
slots.retain(|id, slot| slot.authority.drop_session(id).is_err());
}
pub async fn append_system_context(
&self,
id: &SessionId,
req: &AppendSystemContextRequest,
now_system_time: meerkat_core::time_compat::SystemTime,
now_secs: u64,
) -> Option<Result<AppendSystemContextStatus, meerkat_core::SystemContextStageError>> {
let mut slots = self.slots.write().await;
let slot = slots.get_mut(id)?;
let result = match &mut slot.payload {
StagedPayload::Staged { build_config } => {
if let Err(err) = slot.authority.authorize_system_context_append(id) {
return Some(Err(meerkat_core::SystemContextStageError::InvalidRequest(
format!(
"generated staged-session system-context authority rejected append: {err}"
),
)));
}
let session = build_config
.resume_session
.get_or_insert_with(|| Session::with_id(id.clone()));
let mut state = match session.try_system_context_state() {
Ok(state) => state.unwrap_or_default(),
Err(err) => {
return Some(Err(meerkat_core::SystemContextStageError::InvalidRequest(
format!(
"generated system-context authority rejected staged restore: {err}"
),
)));
}
};
let stage_result = state.stage_append(req, now_system_time);
match stage_result {
Ok(status) => match session.set_system_context_state(state) {
Ok(()) => Ok(status),
Err(_) => {
Err(meerkat_core::SystemContextStageError::InvalidRequest(
"failed to serialize system-context state".to_string(),
))
}
},
Err(e) => Err(e),
}
}
StagedPayload::Promoting {
current_system_context_state,
..
} => {
if let Err(err) = slot.authority.authorize_system_context_append(id) {
return Some(Err(meerkat_core::SystemContextStageError::InvalidRequest(
format!(
"generated staged-session system-context authority rejected append: {err}"
),
)));
}
current_system_context_state.stage_append(req, now_system_time)
}
StagedPayload::Closing { .. } => {
Err(meerkat_core::SystemContextStageError::InvalidRequest(
"session is being archived".to_string(),
))
}
};
if result.is_ok() {
slot.updated_at_secs = now_secs;
}
Some(result)
}
fn slot_info(slot: &StagedSlot) -> Result<StagedSessionInfo, StagedLifecycleError> {
Ok(StagedSessionInfo {
labels: slot.labels.clone().unwrap_or_default(),
effective_llm_identity: slot.authority.effective_llm_identity("slot_info")?,
created_at_secs: slot.created_at_secs,
updated_at_secs: slot.updated_at_secs,
is_promoting: matches!(
slot.authority.phase(),
machine_dsl::StagedSessionPhase::Promoting
| machine_dsl::StagedSessionPhase::Closing
),
})
}
fn matches_label_filter(
slot_labels: Option<&BTreeMap<String, String>>,
filter: Option<&BTreeMap<String, String>>,
) -> bool {
let Some(filter) = filter else {
return true;
};
let Some(slot_labels) = slot_labels else {
return filter.is_empty();
};
filter.iter().all(|(k, v)| slot_labels.get(k) == Some(v))
}
}
#[cfg(test)]
mod tests {
use super::*;
use meerkat_core::Provider;
fn identity(model: &str) -> SessionLlmIdentity {
SessionLlmIdentity {
model: model.to_string(),
provider: Provider::Other,
self_hosted_server_id: None,
provider_params: None,
auth_binding: None,
}
}
fn slot(id: &SessionId) -> StagedSlot {
StagedSlot::new_staged(
id,
AgentBuildConfig::new("test-model".to_string()),
identity("test-model"),
None,
None,
100,
100,
false,
)
.expect("test staged slot should be accepted by generated authority")
}
#[tokio::test]
async fn stage_then_promote_round_trip() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
assert!(reg.contains(&id).await);
let promoted = reg
.begin_promotion(&id)
.await
.ok()
.flatten()
.expect("Staged slot should promote on first call");
assert_eq!(promoted.created_at_secs, 100);
let info = reg.info(&id).await.unwrap().unwrap();
assert!(info.is_promoting);
let states = reg.take_promoting_system_context_state(&id).await;
assert!(states.is_some());
assert!(!reg.contains(&id).await);
}
#[tokio::test]
async fn stage_then_abandon_without_promote() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
assert!(reg.abandon(&id).await.unwrap());
assert!(!reg.contains(&id).await);
}
#[tokio::test]
async fn begin_promotion_twice_rejects_second_caller() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
let first = reg.begin_promotion(&id).await;
assert!(matches!(first, Ok(Some(_))));
let second = reg.begin_promotion(&id).await;
match second {
Err(StagedLifecycleError::AlreadyPromoting(got)) => assert_eq!(got, id),
Err(other) => panic!("expected AlreadyPromoting, got {other:?}"),
Ok(_) => panic!("expected AlreadyPromoting, got Ok"),
}
}
#[tokio::test]
async fn begin_promotion_missing_returns_none() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
let result = reg.begin_promotion(&id).await;
assert!(matches!(result, Ok(None)));
}
#[tokio::test]
async fn generated_keep_alive_authority_requires_comms_name() {
let id = SessionId::new();
let mut build_config = AgentBuildConfig::new("test-model".to_string());
build_config.keep_alive = true;
let rejected = StagedSlot::new_staged(
&id,
build_config,
identity("test-model"),
None,
None,
100,
100,
false,
);
assert!(matches!(
rejected,
Err(StagedLifecycleError::KeepAliveRequiresCommsName)
));
let reg = StagedSessionRegistry::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
let err = reg.update_keep_alive(&id, true, 101).await.unwrap_err();
assert!(matches!(
err,
StagedLifecycleError::KeepAliveRequiresCommsName
));
assert_eq!(reg.keep_alive(&id).await, Some(false));
}
#[tokio::test]
async fn abandon_promotion_restores_staged() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
let promoted = reg
.begin_promotion(&id)
.await
.ok()
.flatten()
.expect("initial promotion");
assert!(reg.info(&id).await.unwrap().unwrap().is_promoting);
reg.abandon_promotion(
id.clone(),
*promoted.build_config,
promoted.labels,
promoted.deferred_prompt,
promoted.created_at_secs,
promoted.updated_at_secs,
)
.await;
assert!(!reg.info(&id).await.unwrap().unwrap().is_promoting);
let promoted_again = reg
.begin_promotion(&id)
.await
.ok()
.flatten()
.expect("re-promotion after abandon");
assert_eq!(promoted_again.created_at_secs, 100);
}
#[tokio::test]
async fn abandon_promotion_does_not_resurrect_finished_slot() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
let promoted = reg
.begin_promotion(&id)
.await
.ok()
.flatten()
.expect("initial promotion");
assert!(reg.take_promoting_system_context_state(&id).await.is_some());
let restored = reg
.abandon_promotion(
id.clone(),
*promoted.build_config,
promoted.labels,
promoted.deferred_prompt,
promoted.created_at_secs,
promoted.updated_at_secs,
)
.await;
assert!(!restored, "finished promotion must not be restored");
assert!(!reg.contains(&id).await);
}
#[tokio::test]
async fn begin_archive_blocks_promotion_until_restored_or_finished() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
assert!(reg.begin_archive(&id).await.unwrap());
assert!(reg.info(&id).await.unwrap().unwrap().is_promoting);
let promoting = reg.begin_promotion(&id).await;
assert!(matches!(
promoting,
Err(StagedLifecycleError::AlreadyPromoting(_))
));
assert!(reg.restore_archive(&id).await);
assert!(!reg.info(&id).await.unwrap().unwrap().is_promoting);
let promoted = reg
.begin_promotion(&id)
.await
.ok()
.flatten()
.expect("promotion should succeed after archive restore");
assert!(
reg.abandon_promotion(
id.clone(),
*promoted.build_config,
promoted.labels,
promoted.deferred_prompt,
promoted.created_at_secs,
promoted.updated_at_secs,
)
.await
);
assert!(reg.begin_archive(&id).await.unwrap());
assert!(reg.finish_archive(&id).await);
assert!(!reg.contains(&id).await);
}
#[tokio::test]
async fn stage_is_idempotent_guard() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
let result = reg.stage(id.clone(), slot(&id)).await;
assert!(matches!(
result,
Err(StagedLifecycleError::AlreadyStaged(_))
));
}
#[tokio::test]
async fn append_system_context_mutates_staged_slot() {
let reg = StagedSessionRegistry::new();
let id = SessionId::new();
reg.stage(id.clone(), slot(&id)).await.unwrap();
let req = AppendSystemContextRequest {
content: meerkat_core::lifecycle::run_primitive::CoreRenderable::text(
"hello".to_string(),
),
source: Some("test".to_string()),
idempotency_key: Some("k1".to_string()),
source_kind: meerkat_core::session::SystemContextSource::Normal,
peer_response_terminal: None,
};
let now = meerkat_core::time_compat::SystemTime::now();
let res = reg.append_system_context(&id, &req, now, 200).await;
let outcome = res.expect("slot is present");
assert!(outcome.is_ok(), "staged append should succeed");
let info = reg.info(&id).await.unwrap().unwrap();
assert_eq!(info.updated_at_secs, 200);
}
#[tokio::test]
async fn list_filters_by_labels() {
let reg = StagedSessionRegistry::new();
let id_a = SessionId::new();
let id_b = SessionId::new();
let mut slot_a = slot(&id_a);
slot_a.labels = Some(BTreeMap::from([("env".to_string(), "prod".to_string())]));
reg.stage(id_a.clone(), slot_a).await.unwrap();
let mut slot_b = slot(&id_b);
slot_b.labels = Some(BTreeMap::from([("env".to_string(), "dev".to_string())]));
reg.stage(id_b.clone(), slot_b).await.unwrap();
let filter = BTreeMap::from([("env".to_string(), "prod".to_string())]);
let result = reg.list(Some(&filter)).await;
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, id_a);
}
}