From 469845df1c080d55929018b376d625c371be14af Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sun, 4 Oct 2026 18:01:11 +0100 Subject: [PATCH] fix(mcp): only the 10-minute ceiling dismisses interactive waits Short interactive waits returned a bogus 10-minute timeout on first expiry, killing the re-poll pattern and blocking task creation. The dismiss branch now fires only when the requested wait actually reaches the ceiling; shorter waits return running as before. Predicate covered by interactive_timeout asserts. --- src/mcp.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/mcp.rs b/src/mcp.rs index 7144ddf85..97ea9d45d 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -333,6 +333,13 @@ fn wait_deadline(op: &str, wait_seconds: f64) -> Duration { Duration::from_secs_f64(capped) } +/// Only a wait that actually reaches the interactive ceiling dismisses the +/// prompt. Shorter waits return `running` so the agent can re-poll; without +/// this distinction the first short poll would kill every interactive op. +fn interactive_timeout(wait_seconds: f64) -> bool { + wait_seconds >= INTERACTIVE_MAX_WAIT.as_secs_f64() +} + struct BatchExecution { id: String, request: Value, @@ -1092,9 +1099,11 @@ impl GuiClient { } } } - } else if interactive && Instant::now() >= deadline { - // Single-call ceiling for human-paced picks: dismiss the prompt - // and say so plainly instead of blocking forever. + } else if interactive && interactive_timeout(wait_seconds) { + // Only the ceiling itself dismisses: short waits keep the + // classic running response so agents can re-poll. Hitting the + // ten-minute ceiling means nobody is coming: dismiss the prompt + // and say so plainly instead of parking it forever. self.dismiss(gui_id.as_deref()); if let Some(key) = mkey.as_ref() { self.inflight.remove(key); @@ -3217,6 +3226,11 @@ mod tests { assert_eq!(wait_deadline("getpoint", 5.0), Duration::from_secs(5)); assert_eq!(wait_deadline("run", 3600.0), Duration::from_secs(60)); assert_eq!(wait_deadline("run", 5.0), Duration::from_secs(5)); + // The ceiling dismisses; anything below returns running for re-poll. + assert!(!interactive_timeout(5.0)); + assert!(!interactive_timeout(599.0)); + assert!(interactive_timeout(600.0)); + assert!(interactive_timeout(3600.0)); } #[test]