[codex] add code-mode host failure supervision hooks (#30110)

## Why

A process host should be discarded and rebuilt after critical actor or
V8 failure, while the existing in-process production path must keep its
current cell-error semantics. This change establishes that failure
boundary without adding the host process or remote client.

## What changed

- add optional task-failure supervision to the transport-neutral
code-mode session runtime
- report Tokio cell-actor failures and V8 runtime-thread panics to a
host-provided fail-stop handler
- preserve the existing handler-less in-process behavior
- make host-owned cell ID allocation fail before numeric wraparound

## Follow-up

The V8 panic signal surfaced here should also be consumed by the
`InProcessCodeModeSession` manager in a future change so it can fail the
affected cell. This PR intentionally leaves the handler-less in-process
behavior unchanged while putting the required panic tracking in place.

## Stack

This is **2 of 4** in the process-owned code-mode session stack.

- #30108 is merged into `main`
- The next PR targets this branch

## Validation

- `just test -p codex-code-mode` — 53 passed
- `just argument-comment-lint -p codex-code-mode`
- `just fix -p codex-code-mode`
This commit is contained in:
Channing Conger
2026-06-25 15:33:58 -07:00
committed by GitHub
parent e2746fd7e9
commit 6c21297bba
12 changed files with 482 additions and 25 deletions
+64 -1
View File
@@ -5,6 +5,8 @@ mod timers;
mod value;
use std::collections::HashMap;
use std::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
use std::sync::OnceLock;
use std::sync::mpsc as std_mpsc;
use std::thread;
@@ -18,6 +20,8 @@ use codex_protocol::ToolName;
use serde_json::Value as JsonValue;
use tokio::sync::mpsc;
use crate::TaskFailureHandler;
const EXIT_SENTINEL: &str = "__codex_code_mode_exit__";
#[derive(Debug)]
@@ -63,6 +67,7 @@ pub(crate) enum RuntimeEvent {
stored_value_writes: HashMap<String, JsonValue>,
error_text: Option<String>,
},
ThreadPanicked,
}
pub(crate) fn spawn_runtime(
@@ -70,6 +75,7 @@ pub(crate) fn spawn_runtime(
request: ExecuteRequest,
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
pending_mode: PendingRuntimeMode,
task_failure_handler: Option<TaskFailureHandler>,
) -> Result<
(
std_mpsc::Sender<RuntimeCommand>,
@@ -96,7 +102,7 @@ pub(crate) fn spawn_runtime(
stored_values,
};
thread::spawn(move || {
spawn_supervised_runtime_thread(event_tx.clone(), task_failure_handler, move || {
run_runtime(
config,
event_tx,
@@ -114,6 +120,21 @@ pub(crate) fn spawn_runtime(
Ok((command_tx, control_tx, isolate_handle))
}
fn spawn_supervised_runtime_thread(
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
task_failure_handler: Option<TaskFailureHandler>,
runtime: impl FnOnce() + Send + 'static,
) {
thread::spawn(move || {
if catch_unwind(AssertUnwindSafe(runtime)).is_err() {
if let Some(task_failure_handler) = task_failure_handler {
task_failure_handler("code-mode V8 runtime thread panicked".to_string());
}
let _ = event_tx.send(RuntimeEvent::ThreadPanicked);
}
});
}
#[derive(Clone)]
struct RuntimeConfig {
tool_call_id: String,
@@ -336,6 +357,7 @@ mod tests {
use super::RuntimeControlCommand;
use super::RuntimeEvent;
use super::spawn_runtime;
use super::spawn_supervised_runtime_thread;
use crate::FunctionCallOutputContentItem;
fn execute_request(source: &str) -> ExecuteRequest {
@@ -348,6 +370,45 @@ mod tests {
}
}
#[tokio::test]
async fn runtime_thread_panic_before_initialization_is_reported_directly() {
let (event_tx, event_rx) = mpsc::unbounded_channel();
drop(event_rx);
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
spawn_supervised_runtime_thread(
event_tx,
Some(std::sync::Arc::new(move |reason| {
let _ = failure_tx.send(reason);
})),
|| panic!("runtime thread panic probe"),
);
assert_eq!(
tokio::time::timeout(Duration::from_secs(1), failure_rx.recv())
.await
.expect("runtime failure timeout")
.expect("runtime failure"),
"code-mode V8 runtime thread panicked"
);
}
#[tokio::test]
async fn runtime_thread_panic_is_forwarded_without_owner_supervision() {
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
spawn_supervised_runtime_thread(
event_tx,
/*task_failure_handler*/ None,
|| panic!("runtime thread panic probe"),
);
assert!(matches!(
tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
.await
.expect("runtime panic event timeout"),
Some(RuntimeEvent::ThreadPanicked)
));
}
#[tokio::test]
async fn terminate_execution_stops_cpu_bound_module() {
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
@@ -356,6 +417,7 @@ mod tests {
execute_request("while (true) {}"),
event_tx,
PendingRuntimeMode::Continue,
/*task_failure_handler*/ None,
)
.unwrap();
@@ -398,6 +460,7 @@ await new Promise(() => {});
),
event_tx,
PendingRuntimeMode::PauseUntilResumed,
/*task_failure_handler*/ None,
)
.unwrap();