use super::*;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BgKind {
Shell,
Agent,
Monitor,
}
impl BgKind {
#[must_use]
pub(crate) fn slug(self) -> &'static str {
match self {
BgKind::Shell => "shell",
BgKind::Agent => "agent",
BgKind::Monitor => "monitor",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BgState {
Open,
Completed,
Failed,
Killed,
Stopped,
TimedOut,
}
impl BgState {
#[must_use]
pub(crate) fn slug(self) -> &'static str {
match self {
BgState::Open => "open",
BgState::Completed => "completed",
BgState::Failed => "failed",
BgState::Killed => "killed",
BgState::Stopped => "stopped",
BgState::TimedOut => "timed-out",
}
}
pub(crate) fn from_status(status: Option<&str>) -> Self {
match status {
Some("failed") => BgState::Failed,
Some("killed") => BgState::Killed,
Some("stopped") => BgState::Stopped,
_ => BgState::Completed,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct BgTask {
pub(crate) kind: BgKind,
pub(crate) id: Option<String>,
pub(crate) tool_use_id: String,
pub(crate) description: Option<String>,
pub(crate) command: Option<String>,
pub(crate) launched_utc: Option<String>,
pub(crate) lane: String,
pub(crate) output_file: Option<String>,
pub(crate) state: BgState,
pub(crate) returned_utc: Option<String>,
pub(crate) output_bytes: Option<u64>,
pub(crate) output_age_secs: Option<i64>,
pub(crate) ignored_by: Option<String>,
}
impl BgTask {
fn haystack(&self) -> String {
let mut s = self.description.clone().unwrap_or_default();
if let Some(c) = &self.command {
s.push(' ');
s.push_str(c);
}
s
}
pub(crate) fn is_open(&self) -> bool {
self.state == BgState::Open
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct BackgroundReport {
pub(crate) tasks: Vec<BgTask>,
pub(crate) notes: Vec<String>,
pub(crate) scanned_files: usize,
}
impl BackgroundReport {
pub(crate) fn open_counted(&self) -> usize {
self.tasks
.iter()
.filter(|t| t.is_open() && t.ignored_by.is_none())
.count()
}
pub(crate) fn open_ignored(&self) -> usize {
self.tasks
.iter()
.filter(|t| t.is_open() && t.ignored_by.is_some())
.count()
}
pub(crate) fn closed_counts(&self) -> (usize, usize, usize, usize, usize) {
let n = |st: BgState| self.tasks.iter().filter(|t| t.state == st).count();
(
n(BgState::Completed),
n(BgState::Failed),
n(BgState::Killed),
n(BgState::Stopped),
n(BgState::TimedOut),
)
}
pub(crate) fn summary_line(&self) -> String {
let (c, f, k, s, t) = self.closed_counts();
let ignored = self.open_ignored();
let ignored = if ignored > 0 {
format!(" (+{ignored} ignored by the lens)")
} else {
String::new()
};
let timed = if t > 0 {
format!(", {t} timed out")
} else {
String::new()
};
format!(
"{} open{ignored}; {c} completed, {f} failed, {k} killed, {s} stopped{timed}",
self.open_counted()
)
}
}
#[derive(Debug, Default)]
pub(crate) struct BackgroundLens {
pub(crate) since: Option<jiff::Timestamp>,
pub(crate) since_raw: Option<String>,
pub(crate) ignore: Vec<(String, regex::Regex)>,
}
impl BackgroundLens {
pub(crate) fn from_args(since: Option<&str>, ignore: &[String]) -> Result<Self> {
let since_ts = since
.map(crate::time_window::parse_bound)
.transpose()
.map_err(|e| anyhow::anyhow!("--background-since: {e}"))?;
let mut compiled = Vec::new();
for raw in ignore {
let re = regex::Regex::new(raw)
.map_err(|e| anyhow::anyhow!("--ignore-background: bad regex `{raw}`: {e}"))?;
compiled.push((raw.clone(), re));
}
Ok(Self {
since: since_ts,
since_raw: since.map(str::to_string),
ignore: compiled,
})
}
pub(crate) fn is_active(&self) -> bool {
self.since.is_some() || !self.ignore.is_empty()
}
fn ignored_by(&self, t: &BgTask) -> Option<String> {
if let (Some(since), Some(raw)) = (self.since, t.launched_utc.as_deref()) {
if let Ok(ts) = raw.parse::<jiff::Timestamp>() {
if ts < since {
return Some(format!(
"launched before --background-since {}",
self.since_raw.as_deref().unwrap_or("?")
));
}
}
}
let hay = t.haystack();
for (raw, re) in &self.ignore {
if re.is_match(&hay) {
return Some(format!("matches --ignore-background {raw}"));
}
}
None
}
}
pub(crate) fn main_transcript_for(path: &Path) -> PathBuf {
if !crate::subagent::is_subagent_path(path) {
return path.to_path_buf();
}
let mut dir = path.parent();
while let Some(d) = dir {
if d.file_name().and_then(|n| n.to_str()) == Some("subagents") {
if let Some(session_dir) = d.parent() {
return session_dir.with_extension("jsonl");
}
}
dir = d.parent();
}
path.to_path_buf()
}
pub(crate) fn background_report(
target: &Path,
want_subagents: bool,
lens: &BackgroundLens,
) -> Result<BackgroundReport> {
let main = main_transcript_for(target);
let mut files: Vec<PathBuf> = vec![main.clone()];
if crate::subagent::is_subagent_path(target) {
files.push(target.to_path_buf());
} else if want_subagents {
files.extend(crate::subagent::subagent_transcript_files(&main).unwrap_or_default());
}
let mut tasks: BTreeMap<String, BgTask> = BTreeMap::new();
let mut carriers: Vec<Carrier> = Vec::new();
let mut notes: Vec<String> = Vec::new();
let mut scanned = 0usize;
for file in &files {
let Some(mmap) = mmap_bytes(file)? else {
continue;
};
scanned += 1;
let bytes: &[u8] = &mmap;
let lane = crate::subagent::session_id_from_path(file);
let is_main = *file == main;
let mut pos = 0usize;
while pos < bytes.len() {
let end = memchr::memchr(b'\n', &bytes[pos..]).map_or(bytes.len(), |i| pos + i);
let line = &bytes[pos..end];
pos = end + 1;
if !line_is_bg_candidate(line) {
continue;
}
let Ok(Some(rec)) = crate::parse::parse_line(line) else {
continue;
};
ingest_launches(&rec, &lane, &mut tasks);
if is_main {
ingest_carriers(&rec, &mut carriers, &mut notes);
}
}
}
resolve_carriers(&mut tasks, &carriers, &mut notes);
let mut list: Vec<BgTask> = tasks.into_values().collect();
for t in &mut list {
if t.is_open() {
t.ignored_by = lens.ignored_by(t);
stat_output(t);
}
}
list.sort_by(|a, b| {
let rank = |t: &BgTask| match (t.is_open(), t.ignored_by.is_some()) {
(true, false) => 0,
(true, true) => 1,
_ => 2,
};
(rank(a), std::cmp::Reverse(a.launched_utc.clone()))
.cmp(&(rank(b), std::cmp::Reverse(b.launched_utc.clone())))
});
Ok(BackgroundReport {
tasks: list,
notes,
scanned_files: scanned,
})
}