Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.1.63"
version = "0.1.64"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"
Expand Down
31 changes: 18 additions & 13 deletions crates/csa-process/src/idle_watchdog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ const LIVENESS_POLL_INTERVAL: Duration = Duration::from_secs(10);
/// Check whether an idle tool process should be terminated.
///
/// When the tool has been silent (no stdout/stderr) for `idle_timeout`, this
/// function queries [`ToolLiveness::is_alive`] before killing. If the tool is
/// still alive (filesystem activity, live PID, etc.) the idle timer is **reset**
/// via `last_activity`, giving the tool another full `idle_timeout` window.
/// Termination only happens when liveness returns false continuously for
/// `liveness_dead_timeout`.
/// function queries [`ToolLiveness::probe`] before killing. Only **progress
/// signals** (output/log growth) reset the idle timer.
/// Pure "process exists" signals (live PID only) no longer
/// grant unlimited extensions; in that case, termination happens once
/// `liveness_dead_timeout` elapses.
pub(crate) fn should_terminate_for_idle(
last_activity: &mut Instant,
idle_timeout: Duration,
Expand All @@ -29,9 +29,9 @@ pub(crate) fn should_terminate_for_idle(

// Legacy execute_in path has no spool/session directory context.
// Preserve original behavior: idle-timeout means immediate termination.
if session_dir.is_none() {
let Some(dir) = session_dir else {
return true;
}
};

let now = Instant::now();
let should_poll = next_liveness_poll_at
Expand All @@ -41,8 +41,9 @@ pub(crate) fn should_terminate_for_idle(
return false;
}

if session_dir.is_some_and(|dir| ToolLiveness::is_alive(dir) || ToolLiveness::is_working(dir)) {
// Tool is alive: reset the idle timer so it gets another full window.
let signals = ToolLiveness::probe(dir);
if signals.has_progress_signal() {
// Real progress observed: reset idle/death timers and give a fresh window.
*last_activity = now;
*liveness_dead_since = None;
*next_liveness_poll_at = Some(now + LIVENESS_POLL_INTERVAL);
Expand Down Expand Up @@ -103,16 +104,20 @@ mod tests {
}

#[test]
fn test_idle_timer_resets_when_liveness_alive() {
fn test_idle_timer_resets_when_progress_signal_present() {
let tmp = tempfile::tempdir().expect("tempdir");
// Create a fresh lock file with our own PID so is_alive() returns true
// Create a lock file for live PID and a fresh output log to simulate
// concrete progress signal.
let locks_dir = tmp.path().join("locks");
std::fs::create_dir_all(&locks_dir).expect("create locks dir");
std::fs::write(
locks_dir.join("codex.lock"),
format!("{{\"pid\": {}}}", std::process::id()),
)
.expect("write lock");
std::fs::write(tmp.path().join("output.log"), "progress").expect("write output");
std::fs::write(tmp.path().join(".liveness.snapshot"), "output_log_size=0")
.expect("seed snapshot");

let mut dead_since = Some(Instant::now() - Duration::from_secs(5));
let mut next_poll = Some(Instant::now() - Duration::from_secs(1));
Expand All @@ -131,11 +136,11 @@ mod tests {
assert!(!terminate, "should not terminate when tool is alive");
assert!(
dead_since.is_none(),
"liveness=true should reset death timer"
"progress signal should reset death timer"
);
assert!(
last_activity > before,
"liveness=true should reset idle timer"
"progress signal should reset idle timer"
);
}
}
Loading