Enable instant interrupts by default (#51812)

## What changed

Enable `instant_interrupt` by default so new user input can preempt responses and yield foreground code-mode observations. Keep the feature marked as under development and update feature-policy tests to allow this exception.

Explicitly disable instant interrupts in core, TUI, and Python SDK tests that depend on responses completing before steering or processing subsequent input.

GitOrigin-RevId: a8cab2d6c445bfdbd10b0b2abcf7893d32ee8595
(cherry picked from commit 406b0c4446)
This commit is contained in:
pakrym-oai
2026-10-07 18:46:56 -07:00
parent ac9b5b8380
commit a028250541
7 changed files with 33 additions and 5 deletions
@@ -294,6 +294,11 @@ async fn streamed_question_precedes_reply_across_resume(
.with_model("gpt-5.4")
.with_history_mode(ThreadHistoryMode::Paginated)
.with_config(|config| {
// This fixture retains the question by allowing its response to finish.
config
.features
.disable(Feature::InstantInterrupt)
.expect("disable InstantInterrupt feature");
config.experimental_thread_store = ThreadStoreConfig::Local;
})
.build_with_streaming_server(&server)
+13 -1
View File
@@ -1964,7 +1964,16 @@ async fn user_input_does_not_preempt_after_reasoning_item() {
let (server, _completions) =
start_streaming_sse_server(vec![first_chunks, response_completed_chunks("resp-2")]).await;
let codex = build_codex(&server).await;
let codex = test_codex()
.with_config(|config| {
config.update_plan_enabled = true;
config.features.disable(Feature::InstantInterrupt).unwrap();
})
.with_model("gpt-5.4")
.build_with_streaming_server(&server)
.await
.expect("build streaming Codex test session")
.codex;
submit_user_input(&codex, "first prompt").await;
@@ -2390,6 +2399,7 @@ async fn steered_user_input_waits_for_model_continuation_after_mid_turn_compact(
let codex = test_codex()
.with_model("gpt-5.4")
.with_config(|config| {
config.features.disable(Feature::InstantInterrupt).unwrap();
config.model_provider.name = "OpenAI (test)".to_string();
config.model_provider.supports_websockets = false;
config.model_auto_compact_token_limit = Some(200);
@@ -2475,6 +2485,7 @@ async fn steered_user_input_follows_compact_when_only_the_steer_needs_follow_up(
let codex = test_codex()
.with_model("gpt-5.4")
.with_config(|config| {
config.features.disable(Feature::InstantInterrupt).unwrap();
config.model_provider.name = "OpenAI (test)".to_string();
config.model_provider.supports_websockets = false;
config.model_auto_compact_token_limit = Some(200);
@@ -2592,6 +2603,7 @@ async fn steered_user_input_waits_when_tool_output_triggers_compact_before_next_
let test = test_codex()
.with_model("gpt-5.4")
.with_config(|config| {
config.features.disable(Feature::InstantInterrupt).unwrap();
config.model_provider.name = "OpenAI (test)".to_string();
config.model_provider.supports_websockets = false;
config.model_auto_compact_token_limit = Some(200);
@@ -58,6 +58,8 @@ async fn heartbeat_repetitions_keep_later_human_authorization() -> anyhow::Resul
.features
.disable(Feature::EnableRequestCompression)
.unwrap();
// Keep the acknowledgement complete before the next heartbeat is processed.
config.features.disable(Feature::InstantInterrupt).unwrap();
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config
+1 -1
View File
@@ -1159,7 +1159,7 @@ pub const FEATURES: &[FeatureSpec] = &[
id: Feature::InstantInterrupt,
key: "instant_interrupt",
stage: Stage::UnderDevelopment,
default_enabled: false,
default_enabled: true,
},
FeatureSpec {
id: Feature::CodeModeOnly,
+4 -2
View File
@@ -32,7 +32,8 @@ fn sleep_tool_config_rejects_unknown_mode() {
#[test]
fn under_development_features_are_disabled_by_default() {
for spec in crate::FEATURES {
if matches!(spec.stage, Stage::UnderDevelopment) {
// Instant interrupts are enabled by default while retaining their development stage.
if matches!(spec.stage, Stage::UnderDevelopment) && spec.id != Feature::InstantInterrupt {
assert_eq!(
spec.default_enabled, false,
"feature `{}` is under development and must be disabled by default",
@@ -121,7 +122,8 @@ fn codex_apps_mcp_protocol_can_be_enabled_independently_of_generic_mcp() {
#[test]
fn default_enabled_features_are_stable() {
for spec in crate::FEATURES {
if spec.default_enabled {
// Instant interrupts are enabled by default while retaining their development stage.
if spec.default_enabled && spec.id != Feature::InstantInterrupt {
assert!(
matches!(spec.stage, Stage::Stable | Stage::Removed),
"feature `{}` is enabled by default but is not stable/removed ({:?})",
@@ -540,6 +540,12 @@ async fn run_safety_retry(
let model_catalog_path = codex_home.path().join("models.json");
std::fs::write(&model_catalog_path, serde_json::to_vec(&model_catalog)?)?;
let model_catalog_path = toml::Value::String(model_catalog_path.display().to_string());
// The committed-steer case counts usage from the response that finishes before steering.
let instant_interrupt_config = if committed_steer.is_some() {
"instant_interrupt = false"
} else {
""
};
std::fs::write(
codex_home.path().join("config.toml"),
format!(
@@ -557,6 +563,7 @@ stream_max_retries = 0
[features]
goals = true
{instant_interrupt_config}
"#,
server.uri()
),
@@ -20,7 +20,7 @@ def test_turn_steer_adds_follow_up_input(tmp_path) -> None:
)
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
thread = codex.thread_start(config={"features.instant_interrupt": False})
turn = thread.turn("Start a steerable turn.")
harness.responses.wait_for_requests(1)
steer = turn.steer("Use this steering input.")