use super::lease_resource;
use lean_ctx::core::agents::AgentRegistry;
pub(super) struct LeaseSettle {
pub(super) kind: lean_ctx::core::agent_lease::AgentLeaseResourceKindV1,
pub(super) resource_ref: String,
pub(super) agent_id: String,
pub(super) lease_ref: String,
}
impl LeaseSettle {
fn release_local(&self) {
let _ = lean_ctx::core::agent_lease::release_shared(
self.kind,
&self.resource_ref,
&self.agent_id,
&self.lease_ref,
);
}
pub(super) fn acquired(&self, outcome: lean_ctx_hive::bus::leases::Outcome) -> Option<String> {
use lean_ctx_hive::bus::leases::{Outcome, scope_line};
let (lease, resource) = (&self.lease_ref, &self.resource_ref);
Some(match outcome {
Outcome::Granted { expires_at, .. } => format!(
"Lease {lease} granted by Hive: resource={resource} scope=hive hive_expires_at={expires_at}"
),
Outcome::Held {
agent_id: holder,
instance_id,
expires_at,
} => {
self.release_local();
format!(
"Lease HELD (hive): resource={resource} owner={holder} instance={instance_id} \
expires={expires_at}; your local lease {lease} was released"
)
}
Outcome::Vetoed => {
self.release_local();
format!(
"Lease {lease} rejected: a workspace veto bars this agent from \
resource={resource} until the veto is lifted; the local lease was released"
)
}
Outcome::Superseded => format!(
"Lease {lease}: {}",
scope_line(&Outcome::Local("Hive lost the lease".into()))
),
other => format!("Lease {lease}: {}", scope_line(&other)),
})
}
fn renewed(&self, outcome: lean_ctx_hive::bus::leases::Outcome) -> Option<String> {
use lean_ctx_hive::bus::leases::{Outcome, scope_line};
let lease = &self.lease_ref;
Some(match outcome {
Outcome::Vetoed => {
self.release_local();
format!(
"Lease renewal of {lease} rejected: a workspace veto ended it; the local \
lease was released"
)
}
Outcome::Superseded => {
self.release_local();
format!(
"Lease renewal of {lease} rejected: Hive no longer holds it for you \
(expired on the service clock or superseded); the local lease was \
released, acquire it again"
)
}
Outcome::Granted { expires_at, .. } => {
format!("Lease {lease} renewed at Hive: hive_expires_at={expires_at}")
}
other => format!("Lease {lease} renewal: {}", scope_line(&other)),
})
}
}
pub(super) fn acquire(
project_root: &str,
current_agent_id: Option<&str>,
message: Option<&str>,
ttl_hours: Option<u64>,
) -> String {
let Some(agent_id) = current_agent_id else {
return "Error: agent must be registered first".to_string();
};
let Some(target) = message else {
return "Error: message (path or symbol:<name>) is required".to_string();
};
if let Err(error) =
AgentRegistry::load_or_create().require_active_for_presence(agent_id, "acquiring a lease")
{
return format!("Lease rejected: {error}");
}
let (resource_kind, resource_ref) = match lease_resource(project_root, target) {
Ok(resource) => resource,
Err(error) => return format!("Lease rejected: {error}"),
};
let paused = lean_ctx_hive::bus::control::veto_refusal(&[&resource_ref]);
let duration_ms = match ttl_hours.unwrap_or(0) {
0 => 10 * 60 * 1_000,
1 => 60 * 60 * 1_000,
_ => return "Error: ttl_hours for lease must be 0 (10min) or 1 (1h)".to_string(),
};
let request = lean_ctx::core::agent_lease::AgentLeaseRequestV1 {
schema_version: lean_ctx::core::agent_lease::AGENT_LEASE_SCHEMA_VERSION,
lease_request_ref: format!(
"lease-request:{}",
blake3::hash(format!("{agent_id}:{resource_ref}").as_bytes()).to_hex()
),
resource_kind,
resource_ref: resource_ref.clone(),
owner_agent_id: agent_id.to_string(),
duration_ms,
};
match lean_ctx::core::agent_lease::acquire_shared(request) {
Ok(lean_ctx::core::agent_lease::AgentLeaseAcquireV1::Granted(lease)) => {
use lean_ctx_hive::bus::leases::{self, Outcome};
let hive = if let Some(why) = paused {
Outcome::Local(why)
} else {
let settle = LeaseSettle {
kind: resource_kind,
resource_ref: resource_ref.clone(),
agent_id: agent_id.to_string(),
lease_ref: lease.lease_ref.clone(),
};
leases::acquire(
std::path::Path::new(project_root),
agent_id,
target,
duration_ms,
&lease.lease_ref,
move |outcome| settle.acquired(outcome),
)
};
format!(
"Lease granted: {} resource={} owner={} expires_at_epoch_ms={} {}",
lease.lease_ref,
resource_ref,
agent_id,
lease.expires_at_epoch_ms,
leases::scope_line(&hive)
)
}
Ok(lean_ctx::core::agent_lease::AgentLeaseAcquireV1::HeldBy {
owner_agent_id,
lease_ref,
expires_at_epoch_ms,
}) => format!(
"Lease HELD: resource={resource_ref} owner={owner_agent_id} lease_ref={lease_ref} expires={expires_at_epoch_ms}"
),
Err(e) => format!("Lease rejected: {e}"),
}
}
pub(super) fn release(
project_root: &str,
current_agent_id: Option<&str>,
message: Option<&str>,
category: Option<&str>,
) -> String {
let Some(agent_id) = current_agent_id else {
return "Error: agent must be registered first".to_string();
};
let Some(target) = message else {
return "Error: message (same path or symbol:<name>) is required".to_string();
};
let Some(lease_ref) = category else {
return "Error: category (lease_ref from lease_acquire) is required".to_string();
};
let (resource_kind, resource_ref) = match lease_resource(project_root, target) {
Ok(resource) => resource,
Err(error) => return format!("Release failed: {error}"),
};
lean_ctx_hive::bus::leases::release(agent_id, lease_ref);
match lean_ctx::core::agent_lease::release_shared(
resource_kind,
&resource_ref,
agent_id,
lease_ref,
) {
Ok(true) => format!("Lease released: {lease_ref} resource={resource_ref}"),
Ok(false) => format!("No active lease found for resource={resource_ref}"),
Err(e) => format!("Release failed: {e}"),
}
}
pub(super) fn renew(
project_root: &str,
current_agent_id: Option<&str>,
message: Option<&str>,
category: Option<&str>,
ttl_hours: Option<u64>,
) -> String {
let Some(agent_id) = current_agent_id else {
return "Error: agent must be registered first".to_string();
};
let Some(target) = message else {
return "Error: message (same path or symbol:<name>) is required for renew".to_string();
};
let Some(lease_ref) = category else {
return "Error: category (lease_ref from lease_acquire) is required".to_string();
};
if let Err(error) =
AgentRegistry::load_or_create().require_active_for_presence(agent_id, "renewing a lease")
{
return format!("Lease renewal rejected: {error}");
}
let (resource_kind, resource_ref) = match lease_resource(project_root, target) {
Ok(resource) => resource,
Err(error) => return format!("Lease renewal rejected: {error}"),
};
let paused = lean_ctx_hive::bus::control::veto_refusal(&[lease_ref, &resource_ref]);
let duration_ms = match ttl_hours.unwrap_or(0) {
0 => 10 * 60 * 1_000,
1 => 60 * 60 * 1_000,
_ => return "Error: ttl_hours for lease must be 0 (10min) or 1 (1h)".to_string(),
};
match lean_ctx::core::agent_lease::renew_shared(
resource_kind,
&resource_ref,
agent_id,
lease_ref,
duration_ms,
) {
Ok(lease) => {
use lean_ctx_hive::bus::leases::{self, Outcome};
let hive = if let Some(why) = paused {
Outcome::Local(why)
} else {
let settle = LeaseSettle {
kind: resource_kind,
resource_ref: resource_ref.clone(),
agent_id: agent_id.to_string(),
lease_ref: lease_ref.to_string(),
};
leases::renew(
std::path::Path::new(project_root),
agent_id,
lease_ref,
duration_ms,
move |outcome| settle.renewed(outcome),
)
};
format!(
"Lease renewed: {} resource={} owner={} expires_at_epoch_ms={} {}",
lease.lease_ref,
resource_ref,
agent_id,
lease.expires_at_epoch_ms,
leases::scope_line(&hive)
)
}
Err(e) => format!("Lease renewal rejected: {e}"),
}
}