use super::presence_ttl_hours;
use lean_ctx::core::agents::AgentRegistry;
pub(super) fn lease_resource(
project_root: &str,
target: &str,
) -> Result<
(
lean_ctx::core::agent_lease::AgentLeaseResourceKindV1,
String,
),
String,
> {
use lean_ctx::core::agent_lease::{self, AgentLeaseResourceKindV1};
if let Some(symbol) = target.strip_prefix("symbol:") {
return Ok((
AgentLeaseResourceKindV1::Symbol,
agent_lease::project_symbol_resource(project_root, symbol),
));
}
agent_lease::project_path_target_resource(project_root, target)
.map(|resource| (AgentLeaseResourceKindV1::Path, resource))
.map_err(|error| error.to_string())
}
pub(super) fn control_status(project_root: &str, current_agent_id: Option<&str>) -> String {
let registry = AgentRegistry::mutate_locked(|registry| {
registry.cleanup_stale(presence_ttl_hours());
})
.map(|(registry, ())| registry)
.unwrap_or_else(|_| AgentRegistry::load_or_create());
let scoped_agents = registry
.agents
.iter()
.filter(|agent| lean_ctx::core::pathutil::same_project(&agent.project_root, project_root));
let mut presence = std::collections::BTreeMap::from([
("active", 0usize),
("idle", 0usize),
("finished", 0usize),
]);
for agent in scoped_agents {
let key = agent.status.to_string();
if let Some(count) = presence.get_mut(key.as_str()) {
*count += 1;
}
}
let pending_messages = current_agent_id.map_or(0, |agent_id| {
registry
.scratchpad
.iter()
.filter(|entry| {
!entry.read_by.iter().any(|reader| reader == agent_id)
&& entry.from_agent != agent_id
&& (entry.to_agent.is_none() || entry.to_agent.as_deref() == Some(agent_id))
&& entry.project_root.as_deref().is_some_and(|own| {
lean_ctx::core::pathutil::same_project(own, project_root)
})
})
.count()
});
let mut graphs = Vec::new();
let mut queued_nodes = 0usize;
let mut active_nodes = 0usize;
let mut stale_nodes = 0usize;
match lean_ctx::core::work_graph_store::WorkGraphStore::load(project_root) {
Ok(store) => {
for graph_id in store.graph_ids().take(32) {
let Some(graph) = store.graph(graph_id) else {
continue;
};
let observation = graph.observation();
let nodes = observation
.get("nodes")
.and_then(serde_json::Value::as_array)
.cloned()
.unwrap_or_default();
let mut counts = std::collections::BTreeMap::new();
for node in &nodes {
let state = node
.get("status")
.and_then(serde_json::Value::as_str)
.unwrap_or("unknown");
*counts.entry(state.to_string()).or_insert(0usize) += 1;
}
queued_nodes += counts.get("pending").copied().unwrap_or(0);
active_nodes += counts.get("active").copied().unwrap_or(0);
stale_nodes += counts.get("stale").copied().unwrap_or(0);
graphs.push(serde_json::json!({
"graph_id": graph_id,
"revision": store.graph_revision(graph_id).ok(),
"node_counts": counts,
}));
}
}
Err(error) => {
tracing::debug!("lean-ctx: control snapshot could not load Work Graph: {error}");
}
}
let next_action = if stale_nodes > 0 {
"reconcile stale Work Graph executions"
} else if queued_nodes > 0 {
"claim one pending Work Graph node"
} else if active_nodes > 0 {
"continue current work and renew leases"
} else {
"lead may enqueue the next independent task"
};
serde_json::to_string_pretty(&serde_json::json!({
"schema_version": 1,
"project_root": project_root,
"presence": presence,
"pending_messages": pending_messages,
"work_graphs": graphs,
"queued_nodes": queued_nodes,
"active_nodes": active_nodes,
"stale_nodes": stale_nodes,
"next_action": next_action,
}))
.unwrap_or_else(|_| "{\"error\":\"control snapshot serialization failed\"}".to_string())
}