mirror of
https://github.com/openai/codex.git
synced 2026-10-24 13:54:49 +00:00
[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:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user