From 9c6d28812b25225171bb0a20630c919ac5fb860d Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 16 May 2026 15:53:00 -0700 Subject: [PATCH 1/2] goals: stop pausing implicitly on interrupts (#22884) --- codex-rs/core/src/goals.rs | 73 ++---------------------------- codex-rs/core/src/session/tests.rs | 4 +- codex-rs/core/src/tasks/mod.rs | 2 - 3 files changed, 5 insertions(+), 74 deletions(-) diff --git a/codex-rs/core/src/goals.rs b/codex-rs/core/src/goals.rs index 4a7a9a3d0b83..14c090a023d2 100644 --- a/codex-rs/core/src/goals.rs +++ b/codex-rs/core/src/goals.rs @@ -23,13 +23,11 @@ use codex_otel::GOAL_TOKEN_COUNT_METRIC; use codex_protocol::config_types::ModeKind; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; -use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ThreadGoal; use codex_protocol::protocol::ThreadGoalStatus; use codex_protocol::protocol::ThreadGoalUpdatedEvent; use codex_protocol::protocol::TokenUsage; -use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::validate_thread_goal_objective; use codex_rollout::state_db::reconcile_rollout; use codex_thread_store::LocalThreadStore; @@ -153,7 +151,6 @@ pub(crate) enum GoalRuntimeEvent<'a> { MaybeContinueIfIdle, TaskAborted { turn_context: Option<&'a TurnContext>, - reason: TurnAbortReason, }, ExternalMutationStarting, ExternalSet { @@ -384,12 +381,8 @@ impl Session { self.maybe_continue_goal_if_idle_runtime().await; Ok(()) }), - GoalRuntimeEvent::TaskAborted { - turn_context, - reason, - } => Box::pin(async move { - self.handle_thread_goal_task_abort(turn_context, reason) - .await; + GoalRuntimeEvent::TaskAborted { turn_context } => Box::pin(async move { + self.handle_thread_goal_task_abort(turn_context).await; Ok(()) }), GoalRuntimeEvent::ExternalMutationStarting => Box::pin(async move { @@ -886,11 +879,7 @@ impl Session { } } - async fn handle_thread_goal_task_abort( - &self, - turn_context: Option<&TurnContext>, - reason: TurnAbortReason, - ) { + async fn handle_thread_goal_task_abort(&self, turn_context: Option<&TurnContext>) { if let Some(turn_context) = turn_context { self.take_thread_goal_continuation_turn(&turn_context.sub_id) .await; @@ -913,12 +902,6 @@ impl Session { accounting.turn = None; } } - - if reason == TurnAbortReason::Interrupted - && let Err(err) = self.pause_active_thread_goal_for_interrupt().await - { - tracing::warn!("failed to pause active thread goal after interrupt: {err}"); - } } async fn account_thread_goal_progress( @@ -1122,56 +1105,6 @@ impl Session { } } - async fn pause_active_thread_goal_for_interrupt(&self) -> anyhow::Result<()> { - if should_ignore_goal_for_mode(self.collaboration_mode().await.mode) { - return Ok(()); - } - - if !self.enabled(Feature::Goals) { - return Ok(()); - } - - let _continuation_guard = self - .goal_runtime - .continuation_lock - .acquire() - .await - .context("goal continuation semaphore closed")?; - let Some(state_db) = self.state_db_for_thread_goals().await? else { - return Ok(()); - }; - self.account_thread_goal_wall_clock_usage( - &state_db, - codex_state::ThreadGoalAccountingMode::ActiveStatusOnly, - TerminalMetricEmission::Emit, - ) - .await?; - let Some(goal) = state_db - .pause_active_thread_goal(self.conversation_id) - .await? - else { - return Ok(()); - }; - let goal = protocol_goal_from_state(goal); - *self.goal_runtime.budget_limit_reported_goal_id.lock().await = None; - self.goal_runtime - .accounting - .lock() - .await - .wall_clock - .clear_active_goal(); - self.send_event_raw(Event { - id: uuid::Uuid::new_v4().to_string(), - msg: EventMsg::ThreadGoalUpdated(ThreadGoalUpdatedEvent { - thread_id: self.conversation_id, - turn_id: None, - goal, - }), - }) - .await; - Ok(()) - } - async fn restore_thread_goal_runtime_after_resume(&self) -> anyhow::Result<()> { if !self.enabled(Feature::Goals) { return Ok(()); diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index bb858999e48c..4ad68f5cd027 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -8239,7 +8239,7 @@ async fn abort_empty_active_turn_preserves_pending_input() { } #[tokio::test] -async fn interrupt_accounts_active_goal_before_pausing() -> anyhow::Result<()> { +async fn interrupt_accounts_active_goal_without_pausing() -> anyhow::Result<()> { let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await; sess.set_thread_goal( tc.as_ref(), @@ -8269,7 +8269,7 @@ async fn interrupt_accounts_active_goal_before_pausing() -> anyhow::Result<()> { .await? .expect("goal should remain persisted after interrupt"); assert_eq!( - codex_protocol::protocol::ThreadGoalStatus::Paused, + codex_protocol::protocol::ThreadGoalStatus::Active, goal.status ); assert_eq!(70, goal.tokens_used); diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 58ead0fd251f..c1879c9ee2eb 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -511,7 +511,6 @@ impl Session { && let Err(err) = self .goal_runtime_apply(GoalRuntimeEvent::TaskAborted { turn_context: turn_context.as_deref(), - reason: reason.clone(), }) .await { @@ -558,7 +557,6 @@ impl Session { if let Err(err) = self .goal_runtime_apply(GoalRuntimeEvent::TaskAborted { turn_context: turn_context.as_deref(), - reason: reason.clone(), }) .await { From d3bd81155c1b995afc6e3311aac6a2eac252bd9c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 18 May 2026 10:36:43 -0700 Subject: [PATCH 2/2] goals: cover shutdown without active turn (#22884) --- codex-rs/core/src/session/tests.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 6d3cebdcfe5f..94017b20b1cf 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -8283,6 +8283,34 @@ async fn interrupt_accounts_active_goal_without_pausing() -> anyhow::Result<()> Ok(()) } +#[tokio::test] +async fn shutdown_without_active_turn_keeps_active_goal_active() -> anyhow::Result<()> { + let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await; + sess.set_thread_goal( + tc.as_ref(), + SetGoalRequest { + objective: Some("Keep improving the benchmark".to_string()), + status: None, + token_budget: None, + }, + ) + .await?; + + assert!(sess.active_turn.lock().await.is_none()); + assert!(handlers::shutdown(&sess, "shutdown".to_string()).await); + + let goal = sess + .get_thread_goal() + .await? + .expect("goal should remain persisted after shutdown"); + assert_eq!( + codex_protocol::protocol::ThreadGoalStatus::Active, + goal.status + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn active_goal_continuation_runs_again_after_no_tool_turn() -> anyhow::Result<()> { let server = start_mock_server().await;