diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 17423a910f..4b7bf32b80 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -686,6 +686,7 @@ impl AgentControl { ) .await; if result.is_ok() { + self.watchdogs.note_owner_input(agent_id).await; self.state .update_last_task_message(agent_id, last_task_message); } @@ -1264,6 +1265,16 @@ impl AgentControl { self.watchdogs.force_due_for_tests(target_thread_id).await; } + #[cfg(test)] + pub(crate) async fn watchdog_owner_idle_since_is_none_for_tests( + &self, + target_thread_id: ThreadId, + ) -> Option { + self.watchdogs + .owner_idle_since_is_none_for_tests(target_thread_id) + .await + } + #[cfg(test)] pub(crate) async fn set_watchdog_active_helper_for_tests( &self, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index a34f2c82c3..732838cd0d 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -417,6 +417,48 @@ async fn send_input_submits_user_message() { assert_eq!(captured, Some(expected)); } +#[tokio::test] +async fn send_input_resets_watchdog_owner_idle_state() { + let harness = AgentControlHarness::new().await; + let (owner_thread_id, _thread) = harness.start_thread().await; + let target_thread_id = ThreadId::new(); + + harness + .control + .register_watchdog(WatchdogRegistration { + owner_thread_id, + target_thread_id, + child_depth: 0, + interval_s: 30, + prompt: String::new(), + config: harness.config.clone(), + }) + .await + .expect("watchdog registration should succeed"); + + assert_eq!( + harness + .control + .watchdog_owner_idle_since_is_none_for_tests(target_thread_id) + .await, + Some(false) + ); + + harness + .control + .send_input(owner_thread_id, text_input("ping")) + .await + .expect("send_input should succeed"); + + assert_eq!( + harness + .control + .watchdog_owner_idle_since_is_none_for_tests(target_thread_id) + .await, + Some(true) + ); +} + #[test] fn build_agent_inbox_items_emits_function_call_and_output() { let sender_thread_id = ThreadId::new(); diff --git a/codex-rs/core/src/agent/watchdog.rs b/codex-rs/core/src/agent/watchdog.rs index 86b068ed7f..71d4246a83 100644 --- a/codex-rs/core/src/agent/watchdog.rs +++ b/codex-rs/core/src/agent/watchdog.rs @@ -448,6 +448,16 @@ impl WatchdogManager { true } + pub(crate) async fn note_owner_input(&self, owner_thread_id: ThreadId) { + let mut registrations = self.registrations.lock().await; + for entry in registrations.values_mut() { + if entry.registration.owner_thread_id == owner_thread_id { + entry.owner_idle_since = None; + entry.owner_was_running = true; + } + } + } + #[cfg(test)] pub(crate) async fn force_due_for_tests(&self, target_thread_id: ThreadId) { let mut registrations = self.registrations.lock().await; @@ -456,6 +466,17 @@ impl WatchdogManager { } } + #[cfg(test)] + pub(crate) async fn owner_idle_since_is_none_for_tests( + &self, + target_thread_id: ThreadId, + ) -> Option { + let registrations = self.registrations.lock().await; + registrations + .get(&target_thread_id) + .map(|entry| entry.owner_idle_since.is_none()) + } + async fn update_after_spawn( &self, target_thread_id: ThreadId, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index a55d9b3a6f..92c4bfb06f 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -145,7 +145,7 @@ pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option = Some(6); pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1; pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option = None; -pub(crate) const DEFAULT_WATCHDOG_INTERVAL_S: i64 = 60; +pub(crate) const DEFAULT_WATCHDOG_INTERVAL_S: i64 = 10; pub const CONFIG_TOML_FILE: &str = "config.toml"; const OPENAI_BASE_URL_ENV_VAR: &str = "OPENAI_BASE_URL";