From e75d8a2fb0bfe1a4eab66107c9ef94805b93ffeb Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 1 Apr 2026 14:32:16 -0400 Subject: [PATCH] Fix attach hanging on Linux CI when engine exits before attach starts The attach loop's PID liveness fallback used `last_seq > 0` (store path) and `!progress_file_is_empty` (file path) to keep the engine "alive" when no launcher record could be found. These conditions are always true once events exist, so the loop never exited via the PID path after the launcher record was cleaned up by start_run or active_launcher_record_for_run. The store-based terminal status check (the other exit path) only read from the SlateDB DbReader, which may not see data still in the WAL or not yet visible via manifest poll. Adding a disk fallback to read status.json ensures the check works even when the store reader has a stale view. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/src/commands/run/attach.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 3dadaa04c..c3466ff46 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -225,6 +225,7 @@ async fn attach_run_store( .await .ok() .flatten() + .or_else(|| read_status_record(&run_dir.join("status.json"))) .map(|record| record.status) .filter(|status| status.is_terminal()); @@ -251,7 +252,7 @@ async fn attach_run_store( cached_pid = Some(pid); process_alive(pid) } else { - attach_started.elapsed() < ATTACH_STARTUP_GRACE || last_seq > 0 + attach_started.elapsed() < ATTACH_STARTUP_GRACE } } }; @@ -465,7 +466,6 @@ async fn attach_run_files( process_alive(pid) } else { attach_started.elapsed() < ATTACH_STARTUP_GRACE - || !progress_file_is_empty(&progress_path) } } }; @@ -552,12 +552,6 @@ fn read_launcher_pid(run_dir: &Path) -> Option { super::launcher::active_launcher_record_for_run(run_dir).map(|record| record.pid) } -fn progress_file_is_empty(path: &Path) -> bool { - std::fs::metadata(path) - .map(|meta| meta.len() == 0) - .unwrap_or(true) -} - #[allow(clippy::struct_field_names)] #[derive(Debug, Clone, PartialEq, Eq)] struct InterviewPaths {