use super::*;
pub fn lifecycle(subagent: &Subagent, journals: &JournalCache) -> Result<SubagentLifecycle> {
let agent_type = subagent.agent_type.clone();
let description = subagent.description.clone();
let mut started_utc: Option<String> = None;
let mut fork_parent_last_uuid: Option<String> = None;
let mut fork_context_length: Option<u64> = None;
let (head_skipped, head_consumed) = head_records(&subagent.path, |rec| {
if rec.is_type("fork-context-ref") {
fork_parent_last_uuid = rec.parent_last_uuid.clone();
fork_context_length = rec.context_length;
}
if let Some(ts) = &rec.timestamp {
started_utc = Some(ts.clone());
return false; }
true
})?;
let mut completed_utc: Option<String> = None;
let mut terminal_agent_msg = false;
let mut saw_any = false;
let mut newest_decided = false;
let mut pending: Option<PendingToolUse> = None;
let tail_skipped = tail_records(&subagent.path, head_consumed, |rec| {
saw_any = true;
if completed_utc.is_none() {
if let Some(ts) = &rec.timestamp {
completed_utc = Some(ts.clone());
}
}
if !newest_decided {
if let Some(tu) = newest_pending_tool_use(rec) {
pending = Some(tu); newest_decided = true;
} else if record_is_meaningful(rec) {
newest_decided = true; }
}
if !terminal_agent_msg && rec.agent_text().is_some() {
terminal_agent_msg = true;
}
completed_utc.is_none() || !terminal_agent_msg
})?;
let journal_done = journal_reports_completion(subagent, journals);
if journal_done || started_utc.is_none() {
pending = None;
}
let status = if pending.is_some() {
SubagentStatus::Running
} else {
resolve_status(
saw_any,
journal_done,
terminal_agent_msg,
started_utc.is_some(),
)
};
Ok(SubagentLifecycle {
agent_type,
description,
started_utc,
completed_utc,
status,
pending,
skipped_lines: head_skipped + tail_skipped,
fork_parent_last_uuid,
fork_context_length,
})
}
pub(crate) fn newest_pending_tool_use(rec: &Record) -> Option<PendingToolUse> {
if !rec.is_type("assistant") {
return None;
}
let blocks = rec.blocks()?;
let mut chosen: Option<(String, String, Option<String>)> = None;
for b in blocks {
let Block::ToolUse {
id: Some(id),
name: Some(name),
input,
} = b
else {
continue;
};
let command = if name == "Bash" {
input
.as_ref()
.and_then(|v| v.get("command"))
.and_then(serde_json::Value::as_str)
.map(str::to_string)
} else {
None
};
if command
.as_deref()
.is_some_and(crate::bash_danger::is_dangerous_rm)
{
return Some(PendingToolUse {
tool_use_id: id.clone(),
tool_name: name.clone(),
command,
since_utc: rec.timestamp.clone(),
});
}
if chosen.is_none() {
chosen = Some((id.clone(), name.clone(), command));
}
}
chosen.map(|(tool_use_id, tool_name, command)| PendingToolUse {
tool_use_id,
tool_name,
command,
since_utc: rec.timestamp.clone(),
})
}
pub(crate) fn record_is_meaningful(rec: &Record) -> bool {
rec.agent_text().is_some()
|| rec.is_genuine_user()
|| rec
.blocks()
.is_some_and(|bs| bs.iter().any(|b| matches!(b, Block::ToolResult { .. })))
}
pub(crate) fn resolve_status(
saw_any: bool,
journal_done: bool,
terminal_agent_msg: bool,
has_start: bool,
) -> SubagentStatus {
if journal_done || terminal_agent_msg {
SubagentStatus::Completed
} else if saw_any && has_start {
SubagentStatus::Running
} else {
SubagentStatus::Unknown
}
}
#[must_use]
pub fn duration_label(started: Option<&str>, completed: Option<&str>) -> Option<String> {
let s: jiff::Timestamp = started?.parse().ok()?;
let c: jiff::Timestamp = completed?.parse().ok()?;
let secs = (c.as_second() - s.as_second()).max(0);
Some(fmt_secs(secs))
}
pub(crate) fn fmt_secs(total: i64) -> String {
let h = total / 3600;
let m = (total % 3600) / 60;
let s = total % 60;
if h > 0 {
format!("{h}h{m:02}m")
} else if m > 0 {
format!("{m}m{s:02}s")
} else {
format!("{s}s")
}
}