use std::path::Path;
use std::sync::Arc;
use super::driver::{ClaimEnder, UnavailableClaims};
use crate::session_manager::worktree_claude_registry::{ClaudeRegistry, RegistryReader};
use crate::session_manager::worktree_owner_gate::{
OwnerGate, SessionEnd, owner_refusal, release_stale_lock,
};
use crate::session_manager::worktree_ownership::{AgentDelegationState, AgentWorktreeOwner};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum HolderState {
Caller,
Ended,
Live,
Unknown(String),
}
#[async_trait::async_trait]
pub(crate) trait ClaimOwnership: Sync {
async fn holder_state(&self, id: &str) -> HolderState {
HolderState::Unknown(format!("this claim store cannot prove session {id} ended"))
}
async fn tree_gate(&self, path: &Path) -> Result<(), String> {
Err(format!(
"this claim store cannot run the ownership gate for {}",
path.display()
))
}
async fn release_stale_lock(&self, path: &Path) -> Result<(), String> {
Err(format!(
"this claim store cannot judge the lock on {}",
path.display()
))
}
}
impl ClaimOwnership for UnavailableClaims {}
pub(crate) struct CallerOwnership {
callers: Vec<String>,
claude: RegistryReader,
}
impl CallerOwnership {
pub(crate) fn from_env() -> Self {
Self::new(
["TM_MANAGED_SESSION_ID", "CLAUDE_CODE_SESSION_ID"]
.into_iter()
.filter_map(|k| std::env::var(k).ok())
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.collect(),
)
}
pub(crate) fn new(callers: Vec<String>) -> Self {
Self {
callers,
claude: Arc::new(|| ClaudeRegistry::NotRead),
}
}
pub(crate) fn with_claude(mut self, claude: RegistryReader) -> Self {
self.claude = claude;
self
}
fn session_end(&self, claude: &ClaudeRegistry, id: &str) -> SessionEnd {
if self.callers.iter().any(|c| c == id) {
return SessionEnd::Caller;
}
let why = "this process cannot observe tmux to prove it; `tm session prune-worktrees \
--merged-prs` reclaims the tree once it has ended";
match claude.session_end(id) {
None => SessionEnd::Undeterminable(why.into()),
Some(SessionEnd::Undeterminable(more)) => {
SessionEnd::Undeterminable(format!("{why}; {more}"))
}
Some(end) => end,
}
}
}
#[async_trait::async_trait]
impl ClaimOwnership for CallerOwnership {
async fn holder_state(&self, id: &str) -> HolderState {
holder_state_of(self.session_end(&(self.claude)(), id))
}
async fn tree_gate(&self, path: &Path) -> Result<(), String> {
let claude = (self.claude)();
host_tree_gate(path, &|id: &str| self.session_end(&claude, id))
}
async fn release_stale_lock(&self, path: &Path) -> Result<(), String> {
release_stale_lock(path)
}
}
pub(super) async fn gate(
ownership: &dyn ClaimOwnership,
path: &Path,
holders: &[String],
) -> Result<(), String> {
for id in holders {
match ownership.holder_state(id).await {
HolderState::Caller | HolderState::Ended => {}
HolderState::Live => {
return Err(format!(
"session {id} claims it, is live, and is not the session running this \
cleanup — its claim is not this run's to end (#8301)"
));
}
HolderState::Unknown(why) => {
return Err(format!(
"session {id} claims it and nothing proves that session ended: {why} (#8301)"
));
}
}
}
ownership.tree_gate(path).await
}
pub(super) async fn regate<C: ClaimEnder>(
claims: &C,
ownership: &dyn ClaimOwnership,
path: &Path,
) -> Result<Vec<String>, String> {
let holders = claims
.claims_on(path)
.await
.map_err(|e| format!("its claims could not be re-read before removal: {e:#}"))?;
gate(ownership, path, &holders).await?;
Ok(holders)
}
pub(crate) fn host_tree_gate(
path: &Path,
session_end: &dyn Fn(&str) -> SessionEnd,
) -> Result<(), String> {
let agent_state = |_: &AgentWorktreeOwner| AgentDelegationState::Unknown;
match owner_refusal(path, &OwnerGate::host(&agent_state, session_end)) {
Some(why) => Err(why),
None => Ok(()),
}
}
pub(crate) fn holder_state_of(end: SessionEnd) -> HolderState {
match end {
SessionEnd::Caller => HolderState::Caller,
SessionEnd::Ended => HolderState::Ended,
SessionEnd::Live => HolderState::Live,
SessionEnd::Undeterminable(why) => HolderState::Unknown(why),
}
}