diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs
index ccbd85b8e9..1a9b40bfd3 100644
--- a/codex-rs/app-server/src/codex_message_processor.rs
+++ b/codex-rs/app-server/src/codex_message_processor.rs
@@ -255,6 +255,7 @@ use codex_core::CodexThreadTurnContextOverrides;
use codex_core::ForkSnapshot;
use codex_core::NewThread;
use codex_core::RolloutRecorder;
+use codex_core::SessionIdleReason;
use codex_core::SessionMeta;
use codex_core::SessionRuntimeEvent;
use codex_core::SessionRuntimeExtension;
@@ -4562,12 +4563,9 @@ impl CodexMessageProcessor {
self.emit_goal_snapshot(thread_id).await;
// App-server owns resume response and snapshot ordering, so wait
// until those are sent before letting the goal runtime continue.
- if let Err(err) = codex_thread
- .apply_runtime_extension_event(SessionRuntimeEvent::MaybeContinueIfIdle)
- .await
- {
- tracing::warn!("failed to continue active goal after resume: {err}");
- }
+ codex_thread
+ .maybe_start_extension_background_turn(SessionIdleReason::ThreadResumed)
+ .await;
}
}
Err(err) => {
@@ -8584,12 +8582,10 @@ async fn handle_pending_thread_resume_request(
.await;
// App-server owns resume response and snapshot ordering, so wait until
// replay completes before letting the goal runtime start continuation.
- if pending.emit_goal_update
- && let Err(err) = conversation
- .apply_runtime_extension_event(SessionRuntimeEvent::MaybeContinueIfIdle)
- .await
- {
- tracing::warn!("failed to continue active goal after running-thread resume: {err}");
+ if pending.emit_goal_update {
+ conversation
+ .maybe_start_extension_background_turn(SessionIdleReason::ThreadResumed)
+ .await;
}
}
diff --git a/codex-rs/app-server/src/codex_message_processor/goal_handlers.rs b/codex-rs/app-server/src/codex_message_processor/goal_handlers.rs
index 7082232664..9ad0d5dcd7 100644
--- a/codex-rs/app-server/src/codex_message_processor/goal_handlers.rs
+++ b/codex-rs/app-server/src/codex_message_processor/goal_handlers.rs
@@ -184,6 +184,11 @@ impl CodexMessageProcessor {
runtime
.apply_external_goal_set(thread.runtime_handle(), goal_status)
.await;
+ if goal_status == codex_state::ThreadGoalStatus::Active {
+ thread
+ .maybe_start_extension_background_turn(SessionIdleReason::HostRequest)
+ .await;
+ }
}
}
diff --git a/codex-rs/app-server/src/goal_runtime/accounting.rs b/codex-rs/app-server/src/goal_runtime/accounting.rs
index 07dc69c76b..841ba2b483 100644
--- a/codex-rs/app-server/src/goal_runtime/accounting.rs
+++ b/codex-rs/app-server/src/goal_runtime/accounting.rs
@@ -9,6 +9,8 @@ use super::state::BudgetLimitSteering;
use super::state::GoalContinuationCandidate;
use super::state::GoalTurnAccountingSnapshot;
use anyhow::Context;
+use codex_core::SessionBackgroundTurn;
+use codex_core::SessionIdleReason;
use codex_core::SessionRuntimeEvent;
use codex_core::SessionRuntimeHandle;
use codex_protocol::ThreadId;
@@ -65,10 +67,6 @@ impl GoalRuntime {
.await;
Ok(())
}
- SessionRuntimeEvent::MaybeContinueIfIdle => {
- self.maybe_continue_goal_if_idle_runtime(&handle).await;
- Ok(())
- }
SessionRuntimeEvent::TaskAborted { turn_id, reason } => {
self.handle_goal_task_abort(&handle, turn_id, reason).await;
Ok(())
@@ -117,7 +115,6 @@ impl GoalRuntime {
}
Ok(None) => {}
}
- self.maybe_continue_goal_if_idle_runtime(handle).await;
}
codex_state::ThreadGoalStatus::BudgetLimited => {
if !handle.has_active_turn().await {
@@ -590,45 +587,22 @@ impl GoalRuntime {
Ok(true)
}
- async fn maybe_continue_goal_if_idle_runtime(&self, handle: &SessionRuntimeHandle) {
- self.maybe_start_goal_continuation_turn(handle).await;
- }
-
- async fn maybe_start_goal_continuation_turn(&self, handle: &SessionRuntimeHandle) {
+ pub(super) async fn provide_idle_background_turn(
+ &self,
+ handle: SessionRuntimeHandle,
+ _reason: SessionIdleReason,
+ ) -> anyhow::Result