use super::*;
#[derive(Debug, Clone)]
pub(crate) struct LaneRef {
pub(crate) path: PathBuf,
pub(crate) exact: bool,
pub(crate) via: &'static str,
}
#[derive(Debug, Clone)]
pub(crate) struct LaneFacts {
pub(crate) lane: String,
pub(crate) session: String,
pub(crate) session_path: PathBuf,
pub(crate) path: PathBuf,
pub(crate) kind: ReceiverKind,
pub(crate) routing_id: Option<String>,
pub(crate) depth: Option<usize>,
pub(crate) parent_agent_id: Option<String>,
pub(crate) state: ReceiverState,
pub(crate) version: Option<String>,
pub(crate) cwd: Option<String>,
pub(crate) last_activity_utc: Option<String>,
pub(crate) socket_present: bool,
pub(crate) headless: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct LaneRow {
pub(crate) lane: String,
pub(crate) kind: ReceiverKind,
pub(crate) state: String,
pub(crate) last_activity_utc: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ParentFacts {
pub(crate) lane: String,
pub(crate) kind: ReceiverKind,
pub(crate) alive: Option<bool>,
pub(crate) reply_channel: &'static str,
}
#[derive(Debug, Clone)]
pub(crate) struct Sections {
pub(crate) me: LaneFacts,
pub(crate) exact: bool,
pub(crate) via: &'static str,
pub(crate) parent: Option<ParentFacts>,
pub(crate) subtree: Vec<LaneRow>,
pub(crate) others: usize,
}
pub(crate) fn lane_facts(path: &Path) -> Result<LaneFacts> {
let lane = crate::subagent::session_id_from_path(path);
let is_sub = crate::subagent::is_subagent_path(path);
let session_path = caller::session_transcript_for(path);
let session = crate::subagent::session_id_from_path(&session_path);
let (version, cwd) = head_facts(path)?;
let shape = tail_shape(path)?;
let reg = if is_sub {
None
} else {
registry_facts(&session).unwrap_or_default()
};
let mut facts = LaneFacts {
lane: lane.clone(),
session,
session_path,
path: path.to_path_buf(),
kind: ReceiverKind::TopLevel,
routing_id: None,
depth: None,
parent_agent_id: None,
state: ReceiverState::Unknown,
version,
cwd,
last_activity_utc: shape.last_ts_utc.clone(),
socket_present: reg.as_ref().is_some_and(|r| r.socket_present),
headless: reg.as_ref().is_some_and(|r| r.headless),
};
if is_sub {
let nodes = crate::subagent::build_topology(&facts.session_path, false).unwrap_or_default();
match nodes.iter().find(|n| n.agent_id == lane) {
Some(n) => {
facts.kind = receiver_kind_of(n.kind);
facts.routing_id =
crate::subagent::routing_id(n.name.as_deref(), n.team_name.as_deref())
.filter(|_| n.kind == crate::subagent::SubagentKind::Teammate);
facts.depth = Some(n.depth);
facts.parent_agent_id = n.parent_agent_id.clone();
}
None => facts.kind = ReceiverKind::UnnamedSubagent,
}
facts.state = lane_state(&shape, path);
} else {
facts.depth = Some(0);
facts.state = session_state(reg.as_ref());
}
Ok(facts)
}
pub(crate) fn receiver_kind_of(kind: crate::subagent::SubagentKind) -> ReceiverKind {
match kind {
crate::subagent::SubagentKind::Teammate => ReceiverKind::Teammate,
crate::subagent::SubagentKind::Workflow => ReceiverKind::WorkflowLane,
crate::subagent::SubagentKind::BuiltinTask => ReceiverKind::UnnamedSubagent,
}
}
fn lane_state(shape: &TailShape, path: &Path) -> ReceiverState {
if shape.records_seen == 0 {
return ReceiverState::Unknown;
}
if shape.unreturned_use.is_some() {
return ReceiverState::Frozen;
}
let clean = shape.last_stop_reason.as_deref() == Some("end_turn");
let live_children = children_report(path, &HashSet::new())
.map(|r| r.live_count)
.unwrap_or(0);
if clean && live_children == 0 {
ReceiverState::Completed
} else {
ReceiverState::Running
}
}
fn session_state(reg: Option<&RegistryFacts>) -> ReceiverState {
let Some(reg) = reg else {
return ReceiverState::Unknown;
};
let Some(pid) = reg.pid else {
return ReceiverState::Unknown;
};
match probe_pid(pid, reg.proc_start.as_deref(), reg.pid_domain.as_deref()) {
PidLiveness::Alive { .. } => ReceiverState::Running,
PidLiveness::Dead | PidLiveness::Reused => ReceiverState::Dead,
PidLiveness::ForeignDomain(_) | PidLiveness::Unavailable => ReceiverState::Unknown,
}
}
pub(crate) fn head_facts(path: &Path) -> Result<(Option<String>, Option<String>)> {
let mut version = None;
let mut cwd = None;
crate::parse::head_records(path, |rec| {
if version.is_none() {
version.clone_from(&rec.version);
}
if cwd.is_none() {
cwd.clone_from(&rec.cwd);
}
version.is_none() || cwd.is_none()
})
.with_context(|| format!("reading the head of {}", path.display()))?;
Ok((version, cwd))
}
pub(crate) fn build_sections(r: &LaneRef) -> Result<Sections> {
let me = lane_facts(&r.path)?;
let subtree_ids = subtree_ids(&me);
let subtree = live_child_lanes(&me.session_path, subtree_ids.as_ref());
let parent = parent_facts(&me)?;
let mine: HashSet<&str> = subtree
.iter()
.map(|l| l.lane.as_str())
.chain(std::iter::once(me.lane.as_str()))
.collect();
let others = live_lane_census()
.unwrap_or_default()
.iter()
.filter(|p| !mine.contains(p.lane.as_str()))
.count();
Ok(Sections {
me,
exact: r.exact,
via: r.via,
parent,
subtree,
others,
})
}
fn subtree_ids(me: &LaneFacts) -> Option<HashSet<String>> {
if me.kind == ReceiverKind::TopLevel {
return None;
}
let nodes = crate::subagent::build_topology(&me.session_path, false).unwrap_or_default();
let mut ids: HashSet<String> = HashSet::new();
loop {
let before = ids.len();
for n in &nodes {
let Some(parent) = n.parent_agent_id.as_deref() else {
continue;
};
if parent == me.lane || ids.contains(parent) {
ids.insert(n.agent_id.clone());
}
}
if ids.len() == before {
break;
}
}
Some(ids)
}
pub(crate) fn live_child_lanes(
session_path: &Path,
only: Option<&HashSet<String>>,
) -> Vec<LaneRow> {
let Ok(report) = children_report(session_path, &HashSet::new()) else {
return Vec::new();
};
let subs = crate::subagent::discover_subagents(session_path).unwrap_or_default();
report
.children
.iter()
.filter(|c| c.state != "settled")
.filter(|c| only.is_none_or(|set| set.contains(&c.session_id)))
.map(|c| {
let sub = subs.iter().find(|s| s.agent_id == c.session_id);
LaneRow {
lane: c.session_id.clone(),
kind: sub.map_or(ReceiverKind::UnnamedSubagent, |s| receiver_kind_of(s.kind)),
state: c.state.to_string(),
last_activity_utc: sub
.and_then(|s| tail_shape(&s.path).ok())
.and_then(|t| t.last_ts_utc),
}
})
.collect()
}
fn parent_facts(me: &LaneFacts) -> Result<Option<ParentFacts>> {
if me.kind == ReceiverKind::TopLevel {
return Ok(None);
}
let path = match me.parent_agent_id.as_deref() {
Some(agent) => crate::subagent::subagent_transcript_files(&me.session_path)
.unwrap_or_default()
.into_iter()
.find(|p| crate::subagent::session_id_from_path(p) == agent),
None => None,
};
let parent = lane_facts(path.as_deref().unwrap_or(&me.session_path))?;
let alive = match parent.state {
ReceiverState::Running | ReceiverState::Frozen => Some(true),
ReceiverState::Completed | ReceiverState::Dead | ReceiverState::StoppedByUser => {
Some(false)
}
ReceiverState::Unknown => None,
};
Ok(Some(ParentFacts {
lane: parent.lane.clone(),
kind: parent.kind,
alive,
reply_channel: reply_channel(me, &parent),
}))
}
fn reply_channel(me: &LaneFacts, parent: &LaneFacts) -> &'static str {
let merged = crate::path::claude_home()
.map(|home| settings::merged(&home, me.cwd.as_deref().map(Path::new)))
.unwrap_or_else(|_| settings::merged(Path::new(""), None));
let slots = ReachSlots::read(&merged);
let caller = Caller {
kind: SenderKind::Lane,
session: Some(me.session.clone()),
lane: Some(me.lane.clone()),
label: None,
lane_exact: true,
};
let ctx = send_context(
&caller,
&target_facts(parent),
&merged,
&slots,
armed_for(parent).len(),
Relation::Child,
);
policy::decide(&ctx).channel
}
pub(crate) fn armed_for(lane: &LaneFacts) -> Vec<u32> {
let root = channel_dir(&lane.session_path.with_extension(""));
read_armed(&root, &lane.lane)
.ok()
.flatten()
.map(|m| m.slots_seen.iter().copied().collect())
.unwrap_or_default()
}