From 366a223d368055ad0e36493db85ef2911b79efad Mon Sep 17 00:00:00 2001 From: Paul Querna Date: Wed, 29 Jul 2026 17:09:58 +0000 Subject: [PATCH] fix(windows): latch shutdown wakeup in parent_pid_monitor The Windows arm of parent_pid_monitor re-created shutdown.notified() on every loop iteration. Notify::notify_waiters() stores no permit, so a shutdown firing in the gap between the sleep arm resolving and the next notified() poll was missed, leaving the monitor polling until parent death. Impact was bounded (the daemon is already shutting down and the process handle is reclaimed on exit), but the fix is one pin: hold a single polled-but-pending Notified future across iterations so the wakeup is latched and the next select! poll completes immediately. Verified: cargo check + clippy --all-targets -D warnings for x86_64-pc-windows-msvc, cargo fmt --check, cargo test -p agent-tui-daemon (Linux aarch64). Co-authored-by: c1-squire-dev[bot] --- crates/agent-tui-daemon/src/server.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/agent-tui-daemon/src/server.rs b/crates/agent-tui-daemon/src/server.rs index 8c4cb95..56df12d 100644 --- a/crates/agent-tui-daemon/src/server.rs +++ b/crates/agent-tui-daemon/src/server.rs @@ -1413,12 +1413,20 @@ async fn parent_pid_monitor(pid: u32, shutdown: Arc) { // would make this task's future non-`Send` and thus unspawnable. Cast // back to `HANDLE` for each syscall. let handle_addr = handle as usize; + // Hold ONE `Notified` future across loop iterations. `notify_waiters()` + // stores no permit, so a future re-created each iteration misses a + // shutdown that fires in the gap between the sleep arm resolving and + // the next `notified()` being polled — the monitor would then keep + // polling until parent death. A polled-but-pending `Notified` stays + // registered, so the wakeup is latched and the next `select!` poll + // completes immediately. + let mut notified = std::pin::pin!(shutdown.notified()); loop { tokio::select! { // External shutdown (idle timeout / `daemon shutdown` / `die`): // stop polling, release the handle, and let the runtime drop // this task cleanly. - () = shutdown.notified() => { + () = &mut notified => { // SAFETY: releasing the handle we opened above. #[allow(unsafe_code)] unsafe { CloseHandle(handle_addr as HANDLE) };