use std::collections::{BTreeMap, BTreeSet};
use std::time::{Duration, Instant};
use futures::{StreamExt, stream};
use super::contracts::{AgentCustomizer, TopologyProvider};
use super::runtime::{EmbodimentOverrides, IdentityRuntime, IdentityRuntimeError};
use super::types::{
AgentBuildDraft, AgentIdentity, ContinuityFailure, ContinuityFailureKind, ContinuityRecord,
ContinuityResolveState, DurableAgentSpec, IdentityLifecycleState, ManagedPeerEdge,
TopologyContext,
};
pub(crate) const IDENTITY_RESTORE_CONCURRENCY: usize = 4;
pub(crate) fn identity_restore_concurrency() -> usize {
parse_identity_restore_concurrency(
std::env::var("MOBKIT_IDENTITY_RESTORE_CONCURRENCY")
.ok()
.as_deref(),
)
}
fn parse_identity_restore_concurrency(raw: Option<&str>) -> usize {
raw.and_then(|value| value.trim().parse::<usize>().ok())
.map(|value| value.clamp(1, 16))
.unwrap_or(IDENTITY_RESTORE_CONCURRENCY)
}
#[cfg(test)]
mod restore_concurrency_tests {
use super::{IDENTITY_RESTORE_CONCURRENCY, parse_identity_restore_concurrency};
#[test]
fn defaults_when_unset_or_invalid() {
assert_eq!(
parse_identity_restore_concurrency(None),
IDENTITY_RESTORE_CONCURRENCY
);
assert_eq!(
parse_identity_restore_concurrency(Some("")),
IDENTITY_RESTORE_CONCURRENCY
);
assert_eq!(
parse_identity_restore_concurrency(Some("not-a-number")),
IDENTITY_RESTORE_CONCURRENCY
);
}
#[test]
fn parses_and_clamps() {
assert_eq!(parse_identity_restore_concurrency(Some("1")), 1);
assert_eq!(parse_identity_restore_concurrency(Some(" 8 ")), 8);
assert_eq!(parse_identity_restore_concurrency(Some("0")), 1);
assert_eq!(parse_identity_restore_concurrency(Some("64")), 16);
}
}
fn trace_identity_restore_completed(identity: &AgentIdentity, started_at: Instant) {
let elapsed = started_at.elapsed();
if elapsed >= Duration::from_secs(1) {
tracing::info!(
%identity,
elapsed_ms = elapsed.as_millis(),
"identity restore completed"
);
} else {
tracing::debug!(
%identity,
elapsed_ms = elapsed.as_millis(),
"identity restore completed"
);
}
}
#[derive(Debug, Clone)]
pub enum RestoreOutcome {
Dormant {
record: Option<ContinuityRecord>,
draft: AgentBuildDraft,
},
Created {
record: ContinuityRecord,
draft: AgentBuildDraft,
},
Resumed {
record: ContinuityRecord,
draft: AgentBuildDraft,
},
Broken(ContinuityFailure),
}
#[derive(Debug)]
pub struct RestoreFlowResult {
pub outcomes: BTreeMap<AgentIdentity, RestoreOutcome>,
pub managed_edges: Vec<ManagedPeerEdge>,
}
#[derive(Debug, Clone)]
pub enum ReconcileAction {
Activate(DurableAgentSpec),
Retire(AgentIdentity),
HotReload {
identity: AgentIdentity,
new_spec: DurableAgentSpec,
},
Respawn {
identity: AgentIdentity,
new_spec: DurableAgentSpec,
},
}
pub fn compute_reconcile_actions(
desired: &[DurableAgentSpec],
current: &BTreeMap<AgentIdentity, DurableAgentSpec>,
) -> Vec<ReconcileAction> {
let mut actions = Vec::new();
let desired_map: BTreeMap<&AgentIdentity, &DurableAgentSpec> =
desired.iter().map(|s| (&s.identity, s)).collect();
for (id, spec) in &desired_map {
if !current.contains_key(*id) {
actions.push(ReconcileAction::Activate((*spec).clone()));
}
}
for id in current.keys() {
if !desired_map.contains_key(id) {
actions.push(ReconcileAction::Retire(id.clone()));
}
}
for (id, new_spec) in &desired_map {
if let Some(old_spec) = current.get(*id)
&& old_spec != *new_spec
{
if old_spec.profile == new_spec.profile {
actions.push(ReconcileAction::HotReload {
identity: (*id).clone(),
new_spec: (*new_spec).clone(),
});
} else {
actions.push(ReconcileAction::Respawn {
identity: (*id).clone(),
new_spec: (*new_spec).clone(),
});
}
}
}
actions
}
type RosterAuthorityGuards = (
tokio::sync::OwnedMutexGuard<()>,
tokio::sync::OwnedMutexGuard<()>,
);
async fn acquire_roster_authority_guards(
runtime: &IdentityRuntime,
identities: &[AgentIdentity],
) -> Result<BTreeMap<AgentIdentity, RosterAuthorityGuards>, IdentityRuntimeError> {
let ordered = identities.iter().cloned().collect::<BTreeSet<_>>();
let mut lifecycle_guards = BTreeMap::new();
for identity in &ordered {
lifecycle_guards.insert(
identity.clone(),
runtime
.lifecycle_lock_for(identity)
.await
.lock_owned()
.await,
);
}
let mut alias_guards = BTreeMap::new();
for identity in &ordered {
alias_guards.insert(
identity.clone(),
runtime
.raw_member_alias_lock(identity.as_str())
.await
.lock_owned()
.await,
);
}
for identity in &ordered {
runtime.ensure_raw_member_alias_available(identity).await?;
}
ordered
.into_iter()
.map(|identity| {
let lifecycle = lifecycle_guards.remove(&identity).ok_or_else(|| {
IdentityRuntimeError::Internal(format!(
"authority gate lost lifecycle reservation for {identity}"
))
})?;
let alias = alias_guards.remove(&identity).ok_or_else(|| {
IdentityRuntimeError::Internal(format!(
"authority gate lost alias reservation for {identity}"
))
})?;
Ok((identity, (lifecycle, alias)))
})
.collect::<Result<_, IdentityRuntimeError>>()
}
pub async fn restore_flow(
runtime: &IdentityRuntime,
roster: &[DurableAgentSpec],
topology_provider: Option<&dyn TopologyProvider>,
customizer: Option<&dyn AgentCustomizer>,
) -> Result<RestoreFlowResult, IdentityRuntimeError> {
let registered = register_roster_metadata(runtime, roster, topology_provider, false).await?;
let managed_edges = registered.managed_edges;
let registered_outcomes = registered.outcomes;
let restore_concurrency = identity_restore_concurrency();
tracing::info!(
member_count = roster.len(),
concurrency = restore_concurrency,
"starting identity restore"
);
let restore_started_at = Instant::now();
let mut restored = stream::iter(roster.iter().cloned().enumerate())
.map(|(index, spec)| {
let initial = registered_outcomes.get(&spec.identity).cloned();
async move {
let identity = spec.identity.clone();
let member_started_at = Instant::now();
let outcome = match initial {
Some(RestoreOutcome::Broken(failure)) => {
RestoreOutcome::Broken(failure)
}
Some(
RestoreOutcome::Dormant { .. }
| RestoreOutcome::Created { .. }
| RestoreOutcome::Resumed { .. },
) => {
let mut bound_bootstrap_generation = None;
match runtime
.embody_identity(
&identity,
None,
None,
None,
&mut bound_bootstrap_generation,
EmbodimentOverrides {
spec: Some(&spec),
customizer,
},
)
.await
{
Ok(embodiment) if embodiment.resumed => RestoreOutcome::Resumed {
record: embodiment.record,
draft: embodiment.draft,
},
Ok(embodiment) => RestoreOutcome::Created {
record: embodiment.record,
draft: embodiment.draft,
},
Err(error) => {
let failure = runtime
.park_embodiment_failure(&identity, &error)
.await;
tracing::warn!(
%identity,
kind = ?failure.kind,
detail = %failure.detail,
"identity embodiment failed; parked Broken while fleet restore continues"
);
RestoreOutcome::Broken(failure)
}
}
}
None => {
let error = IdentityRuntimeError::Internal(format!(
"validated registration outcome disappeared for {identity}"
));
let failure = runtime
.park_embodiment_failure(&identity, &error)
.await;
RestoreOutcome::Broken(failure)
}
};
trace_identity_restore_completed(&identity, member_started_at);
(index, identity, outcome)
}
})
.buffer_unordered(restore_concurrency)
.collect::<Vec<_>>()
.await;
restored.sort_by_key(|(index, _, _)| *index);
let outcomes = restored
.into_iter()
.map(|(_, identity, outcome)| (identity, outcome))
.collect();
tracing::info!(
member_count = roster.len(),
elapsed_ms = restore_started_at.elapsed().as_millis(),
"identity restore completed"
);
runtime.set_desired_peer_edges(managed_edges.clone()).await;
runtime.reconcile_managed_peer_edges(&managed_edges).await?;
Ok(RestoreFlowResult {
outcomes,
managed_edges,
})
}
pub async fn lazy_register_flow(
runtime: &IdentityRuntime,
roster: &[DurableAgentSpec],
topology_provider: Option<&dyn TopologyProvider>,
) -> Result<RestoreFlowResult, IdentityRuntimeError> {
register_roster_metadata(runtime, roster, topology_provider, true).await
}
async fn register_roster_metadata(
runtime: &IdentityRuntime,
roster: &[DurableAgentSpec],
topology_provider: Option<&dyn TopologyProvider>,
update_active_specs: bool,
) -> Result<RestoreFlowResult, IdentityRuntimeError> {
IdentityRuntime::validate_roster_uniqueness(roster)?;
let identities: Vec<AgentIdentity> = roster.iter().map(|s| s.identity.clone()).collect();
let topology_context = TopologyContext {
roster: roster.to_vec(),
};
let managed_edges = if let Some(tp) = topology_provider {
tp.compute_edges(&identities, &topology_context)
.await
.map_err(|e| IdentityRuntimeError::Internal(format!("topology: {e}")))?
} else {
Vec::new()
};
runtime.set_desired_peer_edges(managed_edges.clone()).await;
let mut authority_guards = acquire_roster_authority_guards(runtime, &identities).await?;
runtime.release_parked_unactivated_leases().await?;
let resolved = runtime
.continuity_store()
.resolve_many(&identities)
.await
.map_err(IdentityRuntimeError::Store)?;
for identity in &identities {
if !resolved.contains_key(identity) {
return Err(IdentityRuntimeError::Internal(format!(
"resolve_many did not return state for {identity}"
)));
}
}
let mut outcomes = BTreeMap::new();
for spec in roster {
let identity = &spec.identity;
let _authority_guards = authority_guards.remove(identity).ok_or_else(|| {
IdentityRuntimeError::Internal(format!(
"validated lazy authority reservation disappeared for {identity}"
))
})?;
let resolve_state = resolved.get(identity).cloned().ok_or_else(|| {
IdentityRuntimeError::Internal(format!(
"validated lazy resolve state disappeared for {identity}"
))
})?;
let current_state = runtime
.status(identity)
.await
.ok()
.map(|status| status.state);
let currently_active = current_state == Some(IdentityLifecycleState::Active);
let terminal_verdict = if currently_active {
None
} else {
runtime.continuity_unrecoverable(identity).await
};
if current_state == Some(IdentityLifecycleState::Broken)
&& terminal_verdict.is_none()
&& let Err(error) = runtime
.prepare_broken_identity_for_registration(identity)
.await
{
let failure = runtime.park_embodiment_failure(identity, &error).await;
tracing::warn!(
%identity,
kind = ?failure.kind,
detail = %failure.detail,
"Broken identity cleanup failed; retained exact authority and continued roster registration"
);
outcomes.insert(identity.clone(), RestoreOutcome::Broken(failure));
continue;
}
let draft = AgentBuildDraft {
model: None,
system_prompt: None,
additional_instructions: spec.additional_instructions.clone(),
labels: spec.labels.clone(),
app_context: spec.context.clone(),
external_tools: Vec::new(),
local_external_tools: Default::default(),
provider_params: None,
compaction_curator: Default::default(),
};
match resolve_state {
ContinuityResolveState::Uninitialized => {
if currently_active {
if update_active_specs {
runtime.update_spec(spec.clone()).await?;
}
} else {
runtime
.register(spec.clone(), IdentityLifecycleState::Dormant, None, None)
.await;
}
outcomes.insert(
identity.clone(),
RestoreOutcome::Dormant {
record: None,
draft,
},
);
}
ContinuityResolveState::Ready { record } => {
if currently_active {
if update_active_specs {
runtime.update_spec(spec.clone()).await?;
}
} else if let Some(verdict) = terminal_verdict {
runtime.update_spec(spec.clone()).await?;
outcomes.insert(
identity.clone(),
RestoreOutcome::Broken(ContinuityFailure {
identity: identity.clone(),
kind: ContinuityFailureKind::CheckpointUnrecoverable,
record: Some(record),
detail: verdict.reason,
}),
);
continue;
} else {
runtime
.register(
spec.clone(),
IdentityLifecycleState::Dormant,
Some(record.clone()),
None,
)
.await;
}
outcomes.insert(
identity.clone(),
RestoreOutcome::Dormant {
record: Some(record),
draft,
},
);
}
ContinuityResolveState::Broken { failure } => {
if currently_active {
if update_active_specs {
runtime.update_spec(spec.clone()).await?;
}
outcomes.insert(
identity.clone(),
RestoreOutcome::Dormant {
record: failure.record,
draft,
},
);
} else if let Some(verdict) = terminal_verdict
&& matches!(failure.kind, ContinuityFailureKind::SnapshotMissing)
&& failure.record.is_some()
{
runtime.update_spec(spec.clone()).await?;
outcomes.insert(
identity.clone(),
RestoreOutcome::Broken(ContinuityFailure {
identity: identity.clone(),
kind: ContinuityFailureKind::CheckpointUnrecoverable,
record: failure.record.clone(),
detail: verdict.reason,
}),
);
} else if matches!(failure.kind, ContinuityFailureKind::SnapshotMissing)
&& let Some(record) = failure.record.clone()
{
if currently_active {
if update_active_specs {
runtime.update_spec(spec.clone()).await?;
}
} else {
runtime
.register(
spec.clone(),
IdentityLifecycleState::Dormant,
Some(record.clone()),
None,
)
.await;
}
outcomes.insert(
identity.clone(),
RestoreOutcome::Dormant {
record: Some(record),
draft,
},
);
} else {
tracing::warn!(
%identity,
kind = ?failure.kind,
detail = %failure.detail,
"restore failure registers the identity Broken pending reconcile"
);
runtime
.register(
spec.clone(),
IdentityLifecycleState::Broken,
failure.record.clone(),
None,
)
.await;
outcomes.insert(identity.clone(), RestoreOutcome::Broken(failure));
}
}
}
}
if update_active_specs
&& let Err(err) = runtime.reconcile_managed_peer_edges(&managed_edges).await
{
tracing::warn!(
error = %err,
"identity lazy register flow completed with topology reconcile warning"
);
}
Ok(RestoreFlowResult {
outcomes,
managed_edges,
})
}