Add unfinished root turn suspension (#40038)

## Why

An active root turn needs to be stopped without marking it complete or aborted
before another runtime can recover the same turn ID.

## What changed

- Add `CodexThread::suspend_turn_and_shutdown` and `SuspendTurnOutcome`.
- Flush history, stop the active regular task, close the history writer, and
  shut down the session without recording a terminal turn event.
- Reject suspension when no supported turn is active or the loaded agent
  subtree still contains a live descendant.

## Testing

- Verify that suspension preserves unfinished history and allows the turn to be
  recovered under its original ID after the descendant guard is cleared.

GitOrigin-RevId: 1f9b019d07c51474ec2d991d263bc15cdd4f89ad
This commit is contained in:
richardopenai authored and copyberry committed 2026-08-22 00:42:19 +00:00
1 parent 970b7f2ff4
commit 4f39251a01
10 files changed
+345 -3

No files matched your search

+15 -2
View File
@@ -394,7 +394,7 @@ pub async fn set_thread_memory_mode(sess: &Arc<Session>, sub_id: String, mode: T
}
}
async fn shutdown_session_runtime(sess: &Arc<Session>) {
pub(super) async fn shutdown_session_runtime(sess: &Arc<Session>) {
if let Some(startup_prewarm) = sess.take_session_startup_prewarm().await {
startup_prewarm.abort().await;
}
@@ -421,7 +421,7 @@ async fn shutdown_session_runtime(sess: &Arc<Session>) {
crate::hook_runtime::run_session_end_hooks(sess).await;
}
async fn emit_thread_stop_lifecycle(sess: &Session) {
pub(super) async fn emit_thread_stop_lifecycle(sess: &Session) {
for contributor in sess.services.extensions.thread_lifecycle_contributors() {
contributor
.on_thread_stop(codex_extension_api::ThreadStopInput {
@@ -585,6 +585,19 @@ pub(super) async fn submission_loop(
let _ = reply.send(result);
false
}
Op::SuspendTurnAndShutdown { reply } => {
let result =
super::turn_suspension::suspend_turn_and_shutdown(&sess, sub.id.clone())
.await;
// Exit only after history is durable and its writer has closed; an error
// must leave responsibility for the thread with the current worker.
let should_exit = matches!(
&result,
Ok(codex_protocol::turn_input::SuspendTurnOutcome::Suspended { .. })
);
let _ = reply.send(result);
should_exit
}
Op::ThreadSettings { thread_settings } => {
thread_settings::update(&sess, sub.id.clone(), thread_settings).await;
false