Add default code-mode yield timeout (#14484)

Summary
- expose the default yield timeout through code mode runtime so the
handler, wait tool, and protocol share the same 10s value that matches
unified exec
- document the timeout change in the tool descriptions and propagate the
value all the way into the runner metadata
- adjust Cargo.lock to keep the dependency tree in sync with the added
code mode tool dependency

Testing
- Not run (not requested)
This commit is contained in:
pakrym-oai
2026-03-12 12:06:23 -07:00
committed by GitHub
parent 3e96c867fe
commit d1b03f0d7f
6 changed files with 106 additions and 1 deletions

View File

@@ -48,6 +48,7 @@ impl CodeModeExecuteHandler {
let message = HostToNodeMessage::Start {
request_id: request_id.clone(),
session_id,
default_yield_time_ms: super::DEFAULT_EXEC_YIELD_TIME_MS,
enabled_tools,
stored_values,
source,

View File

@@ -35,6 +35,7 @@ const CODE_MODE_WAIT_DESCRIPTION_TEMPLATE: &str = include_str!("wait_description
pub(crate) const PUBLIC_TOOL_NAME: &str = "exec";
pub(crate) const WAIT_TOOL_NAME: &str = "exec_wait";
pub(crate) const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000;
pub(crate) const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000;
#[derive(Clone)]

View File

@@ -41,6 +41,7 @@ pub(super) enum HostToNodeMessage {
Start {
request_id: String,
session_id: i32,
default_yield_time_ms: u64,
enabled_tools: Vec<EnabledTool>,
stored_values: HashMap<String, JsonValue>,
source: String,

View File

@@ -572,6 +572,7 @@ function startSession(protocol, sessions, start) {
const session = {
completed: false,
content_items: [],
default_yield_time_ms: normalizeYieldTime(start.default_yield_time_ms),
id: start.session_id,
initial_yield_timer: null,
initial_yield_triggered: false,
@@ -585,6 +586,7 @@ function startSession(protocol, sessions, start) {
}),
};
sessions.set(session.id, session);
scheduleInitialYield(protocol, session, session.default_yield_time_ms);
session.worker.on('message', (message) => {
void handleWorkerMessage(protocol, sessions, session, message).catch((error) => {
@@ -697,6 +699,9 @@ async function sendYielded(protocol, session) {
if (session.completed || session.request_id === null) {
return;
}
session.initial_yield_timer = clearTimer(session.initial_yield_timer);
session.initial_yield_triggered = true;
session.poll_yield_timer = clearTimer(session.poll_yield_timer);
const contentItems = takeContentItems(session);
const requestId = session.request_id;
try {

View File

@@ -68,7 +68,7 @@ struct WriteStdinArgs {
}
fn default_exec_yield_time_ms() -> u64 {
10000
10_000
}
fn default_write_stdin_yield_time_ms() -> u64 {

View File

@@ -466,6 +466,103 @@ output_text("phase 3");
Ok(())
}
#[cfg_attr(windows, ignore = "no exec_command on Windows")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_yield_timeout_works_for_busy_loop() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let mut builder = test_codex().with_config(move |config| {
let _ = config.features.enable(Feature::CodeMode);
});
let test = builder.build(&server).await?;
let code = r#"
import { output_text, set_yield_time } from "@openai/code_mode";
output_text("phase 1");
set_yield_time(10);
while (true) {}
"#;
responses::mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-1"),
ev_custom_tool_call("call-1", "exec", code),
ev_completed("resp-1"),
]),
)
.await;
let first_completion = responses::mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-1", "waiting"),
ev_completed("resp-2"),
]),
)
.await;
tokio::time::timeout(
Duration::from_secs(5),
test.submit_turn("start the busy loop"),
)
.await??;
let first_request = first_completion.single_request();
let first_items = custom_tool_output_items(&first_request, "call-1");
assert_eq!(first_items.len(), 2);
assert_regex_match(
concat!(
r"(?s)\A",
r"Script running with session ID \d+\nWall time \d+\.\d seconds\nOutput:\n\z"
),
text_item(&first_items, 0),
);
assert_eq!(text_item(&first_items, 1), "phase 1");
let session_id = extract_running_session_id(text_item(&first_items, 0));
responses::mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-3"),
responses::ev_function_call(
"call-2",
"exec_wait",
&serde_json::to_string(&serde_json::json!({
"session_id": session_id,
"terminate": true,
}))?,
),
ev_completed("resp-3"),
]),
)
.await;
let second_completion = responses::mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-2", "terminated"),
ev_completed("resp-4"),
]),
)
.await;
test.submit_turn("terminate it").await?;
let second_request = second_completion.single_request();
let second_items = function_tool_output_items(&second_request, "call-2");
assert_eq!(second_items.len(), 1);
assert_regex_match(
concat!(
r"(?s)\A",
r"Script terminated\nWall time \d+\.\d seconds\nOutput:\n\z"
),
text_item(&second_items, 0),
);
Ok(())
}
#[cfg_attr(windows, ignore = "no exec_command on Windows")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_can_run_multiple_yielded_sessions() -> Result<()> {