use super::*;
use std::io::Write as _;
const POLL_FLOOR_MS: u64 = 200;
const POLL_CEIL_MS: u64 = 2000;
const TIMEOUT_EXIT: u8 = 124;
struct Cursor {
path: PathBuf,
offset: u64,
lane: String,
}
pub fn run_wait(args: &WaitArgs) -> Result<()> {
let conds: Vec<(String, Cond)> = args
.until
.iter()
.map(|s| parse_condition(s).map(|c| (s.clone(), c)))
.collect::<Result<Vec<_>>>()?;
let needs_verdict = conds.iter().any(|(_, c)| c.needs_verdict());
let main = resolve_live_target(&args.target, args.want_subagents())?;
let lens = BackgroundLens::from_args(
args.lens.background_since.as_deref(),
&args.lens.ignore_background,
)?;
let Some(timeout_secs) = args.timeout else {
bail!(
"wait needs --timeout <SECS>: a background task can be designed never to return \
(a dev server, a watcher), so a wait without a bound never ends. Pick a bound, \
branch on exit 124, and read the at-exit report; narrow what counts with \
--background-since now / --ignore-background <RE>"
);
};
let is_subagent_target = crate::subagent::is_subagent_path(&main);
let mut cursors: Vec<Cursor> = Vec::new();
let seed = |path: PathBuf, cursors: &mut Vec<Cursor>| {
if cursors.iter().any(|c| c.path == path) {
return;
}
let offset = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
let lane = crate::subagent::session_id_from_path(&path);
cursors.push(Cursor { path, offset, lane });
};
seed(main.clone(), &mut cursors);
if args.want_subagents() && !is_subagent_target {
for sub in crate::subagent::subagent_transcript_files(&main).unwrap_or_default() {
seed(sub, &mut cursors);
}
}
if !is_subagent_target {
if let Some(sc) = crate::elicitation::sidecar_path(&main) {
seed(sc, &mut cursors);
}
}
let start = std::time::Instant::now();
let deadline = std::time::Duration::from_secs(timeout_secs);
eprintln!(
"csift: watching {} file(s) from byte offsets; conditions: {}; timeout {timeout_secs}s{}",
cursors.len(),
args.until.join(" | "),
if lens.is_active() {
format!(
"; lens: since {}, {} ignore pattern(s)",
lens.since_raw.as_deref().unwrap_or("-"),
lens.ignore.len()
)
} else {
String::new()
}
);
let mut activity = Activity::default();
let mut interval = args.interval.unwrap_or(POLL_FLOOR_MS);
loop {
if args.want_subagents() && !is_subagent_target {
for sub in crate::subagent::subagent_transcript_files(&main).unwrap_or_default() {
if !cursors.iter().any(|c| c.path == sub) {
let lane = crate::subagent::session_id_from_path(&sub);
cursors.push(Cursor {
path: sub,
offset: 0,
lane,
});
}
}
}
if !is_subagent_target {
if let Some(sc) = crate::elicitation::sidecar_path(&main) {
if !cursors.iter().any(|c| c.path == sc) {
let lane = crate::subagent::session_id_from_path(&main);
cursors.push(Cursor {
path: sc,
offset: 0,
lane,
});
}
}
}
let mut anything_grew = false;
for cur in &mut cursors {
let len = std::fs::metadata(&cur.path).map(|m| m.len()).unwrap_or(0);
if len < cur.offset {
activity.note_shrink(&cur.lane, cur.offset - len);
cur.offset = len;
continue;
}
if len == cur.offset {
continue;
}
anything_grew = true;
let bytes = read_range(&cur.path, cur.offset, len)?;
let base = cur.offset;
let mut pos = 0usize;
while pos < bytes.len() {
let Some(nl) = memchr::memchr(b'\n', &bytes[pos..]) else {
break; };
let line = &bytes[pos..pos + nl];
pos += nl + 1;
let Ok(Some(rec)) = crate::parse::parse_line(line) else {
continue;
};
activity.fold(&rec, &cur.lane);
for (raw, cond) in &conds {
if record_matches(cond, &rec) {
return finish(args, raw, &main, &lens, &activity, start.elapsed());
}
}
}
cur.offset = base + pos as u64;
}
if needs_verdict {
let assessment = assess_path(&main, args.want_subagents(), &lens)?;
for (raw, cond) in &conds {
if verdict_matches(cond, assessment.verdict) {
return finish(args, raw, &main, &lens, &activity, start.elapsed());
}
}
}
if start.elapsed() >= deadline {
return finish_timeout(args, &main, &lens, &activity, start.elapsed());
}
if args.interval.is_none() {
interval = if anything_grew {
POLL_FLOOR_MS
} else {
(interval * 3 / 2).min(POLL_CEIL_MS)
};
}
std::thread::sleep(std::time::Duration::from_millis(interval));
}
}
fn finish(
args: &WaitArgs,
fired: &str,
main: &Path,
lens: &BackgroundLens,
activity: &Activity,
waited: std::time::Duration,
) -> Result<()> {
let assessment = assess_path(main, args.want_subagents(), lens)?;
emit_wait(args, fired, main, &assessment, activity, waited)?;
Ok(())
}
fn finish_timeout(
args: &WaitArgs,
main: &Path,
lens: &BackgroundLens,
activity: &Activity,
waited: std::time::Duration,
) -> Result<()> {
let assessment = assess_path(main, args.want_subagents(), lens)?;
emit_wait(args, "timeout", main, &assessment, activity, waited)?;
let _ = std::io::stdout().flush();
std::process::exit(i32::from(TIMEOUT_EXIT));
}
fn emit_wait(
args: &WaitArgs,
fired: &str,
main: &Path,
a: &Assessment,
activity: &Activity,
waited: std::time::Duration,
) -> Result<()> {
let session_id = crate::subagent::session_id_from_path(main);
match args.format {
OutputFormat::Text => {
println!("fired {fired}");
println!("verdict {}", a.verdict.slug());
println!("waited {}s", waited.as_secs());
println!("at exit {}", a.tail_state);
println!("activity {}", activity.summary_line());
for e in &a.evidence {
println!(" {:<9} {}", e.surface, e.value);
}
render_background_text(&a.background);
render_last_text(&session_id, &a.last);
for n in a.notes.iter().chain(a.background.notes.iter()) {
println!(" note: {n}");
}
}
OutputFormat::Json => {
let obj = serde_json::json!({
"kind": "wait",
"fired": fired,
"verdict": a.verdict.slug(),
"waited_secs": waited.as_secs(),
"at_exit": a.tail_state,
"activity": activity.json(),
"evidence": a.evidence.iter().map(|e| serde_json::json!({
"surface": e.surface,
"value": e.value,
"age_secs": e.age_secs,
})).collect::<Vec<_>>(),
"background": background_json(&a.background),
"last": last_json(&a.last),
"notes": a.notes.iter().chain(a.background.notes.iter()).collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string(&obj)?);
}
}
Ok(())
}