fix(subagents): release resident file leases on agent completion (#660)

Moved RESIDENT_LEASES from block-scoped static to module level so the
release function can access it. Added release_resident_leases_for()
called at all three terminal transitions:
- Cancelled (cancel_agent)
- Failed (update_failed)
- Completed (run_subagent_turn result construction)

Previously leases were acquired but never released, causing false
conflict warnings on subsequent spawns targeting the same file.
This commit is contained in:
Hunter Bown
2026-05-05 00:54:34 -05:00
parent b51fa6bc91
commit 2ee826924a
+21 -3
View File
@@ -39,6 +39,23 @@ pub use mailbox::{Mailbox, MailboxEnvelope, MailboxMessage, MailboxReceiver};
// === Constants ===
/// Global ownership table for cache-aware resident file sub-agents (#529).
/// Maps file path → agent id. Agents hold a lease on a file while running;
/// the lease is released when the agent reaches a terminal state.
static RESIDENT_LEASES: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<String, String>>,
> = std::sync::OnceLock::new();
/// Release all resident file leases held by `agent_id`. Called when an
/// agent transitions to a terminal state (completed, failed, cancelled).
fn release_resident_leases_for(agent_id: &str) {
if let Some(lock) = RESIDENT_LEASES.get() {
if let Ok(mut guard) = lock.lock() {
guard.retain(|_, owner| owner != agent_id);
}
}
}
const DEFAULT_MAX_STEPS: u32 = 100;
const TOOL_TIMEOUT: Duration = Duration::from_secs(30);
/// Per-step LLM API call timeout. Each `create_message` request must complete
@@ -1071,6 +1088,7 @@ impl SubAgentManager {
let mut changed = false;
if agent.status == SubAgentStatus::Running {
agent.status = SubAgentStatus::Cancelled;
release_resident_leases_for(&agent.id);
if let Some(handle) = agent.task_handle.take() {
handle.abort();
}
@@ -1375,6 +1393,7 @@ impl SubAgentManager {
let mut changed = false;
if let Some(agent) = self.agents.get_mut(agent_id) {
agent.status = SubAgentStatus::Failed(error);
release_resident_leases_for(agent_id);
agent.task_handle = None;
changed = true;
}
@@ -1631,9 +1650,6 @@ impl ToolSpec for AgentSpawnTool {
);
// Check ownership (best-effort, non-blocking).
let conflict = {
static RESIDENT_LEASES: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<String, String>>,
> = std::sync::OnceLock::new();
let leases = RESIDENT_LEASES
.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
let mut guard = leases.lock().unwrap_or_else(|p| p.into_inner());
@@ -2872,6 +2888,8 @@ async fn run_subagent(
}
}
release_resident_leases_for(&agent_id);
Ok(SubAgentResult {
agent_id,
agent_type,