use crate::daemon::Daemon;
use autofork_core::protocol::WakeFork;
use autofork_core::store::SessionRow;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
fn flushable(reason: &str) -> bool {
!matches!(reason, "timeout" | "pruned")
}
pub fn take_final_runs_for_close(
daemon: &Arc<Daemon>,
row: &SessionRow,
reason: &str,
) -> Vec<WakeFork> {
if !flushable(reason) {
return Vec::new();
}
if !daemon.cfg_for(Some(&row.project_root)).flush_on_close {
return Vec::new();
}
if daemon.is_fork_run_session(&row.session_id) {
return Vec::new();
}
crate::planner::build_final_runs(daemon, row)
}
pub fn spawn_end_runner(daemon: &Arc<Daemon>, row: &SessionRow, specs: &[WakeFork]) {
if specs.is_empty() {
return;
}
let Some(cli) = cli_binary() else {
tracing::warn!(session = %row.session_id, "flush-on-close: no autofork CLI to run it with");
return;
};
let tmp = daemon.paths.base.join("tmp");
let _ = std::fs::create_dir_all(&tmp);
let Ok(json) = serde_json::to_string(specs) else {
return;
};
let specs_path = tmp.join(format!(
"final-{}-{}.json",
row.session_id,
crate::daemon::now()
));
if std::fs::write(&specs_path, json).is_err() {
return;
}
let log_path = daemon.paths.base.join("logs/final-run.log");
if let Some(parent) = log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let Ok(log) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
else {
return;
};
let Ok(log2) = log.try_clone() else { return };
let resume_target = row
.transcript_path
.as_deref()
.and_then(|p| p.file_stem())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| row.session_id.clone());
let mut cmd = Command::new(cli);
cmd.arg("final-run")
.arg("--client")
.arg(row.client.as_deref().unwrap_or("claude-code"))
.arg("--session")
.arg(&row.session_id)
.arg("--resume-target")
.arg(&resume_target)
.arg("--cwd")
.arg(&row.cwd)
.arg("--specs")
.arg(&specs_path);
if let Some(model) = row.model.as_deref() {
cmd.arg("--model").arg(model);
}
if let Some(bin) = row.harness.as_ref().and_then(|h| h.bin.as_deref()) {
cmd.arg("--bin").arg(bin);
}
let env = daemon.session_env(&row.session_id);
match env.as_ref() {
Some(env) => {
env.apply(&mut cmd);
tracing::debug!(session = %row.session_id, carried = ?env.set_names(),
"flush-on-close: end-runner runs with the session's own credential env");
}
None => tracing::debug!(session = %row.session_id,
"flush-on-close: no credential env recorded for this session, inheriting the daemon's"),
}
match autofork_core::sys::spawn_detached(&mut cmd, log, log2) {
Ok(pid) => tracing::info!(
session = %row.session_id,
pid,
forks = ?specs.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(),
"flush-on-close: daemon spawned the end-runner"
),
Err(e) => tracing::warn!(session = %row.session_id, error = %e,
"flush-on-close: could not spawn the end-runner"),
}
}
fn cli_binary() -> Option<PathBuf> {
if let Some(over) = std::env::var_os("AUTOFORK_FINAL_RUNNER_BIN") {
return Some(PathBuf::from(over));
}
if let Some(sibling) = std::env::current_exe().ok().and_then(|e| {
e.parent()
.map(|p| p.join(format!("autofork{}", autofork_core::sys::EXE_SUFFIX)))
}) {
if sibling.is_file() {
return Some(sibling);
}
}
Some(PathBuf::from("autofork"))
}