diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 576f031565..021505caaf 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -15,7 +15,10 @@ use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStatus; +use codex_app_server_protocol::TurnInterruptParams; +use codex_app_server_protocol::TurnInterruptResponse; use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; use codex_protocol::config_types::Personality; @@ -358,9 +361,10 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { .await??; primary.clear_message_buffer(); - let running_turn_id = primary + let thread_id = thread.id.clone(); + let running_turn_request_id = primary .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), + thread_id: thread_id.clone(), input: vec![UserInput::Text { text: "keep running".to_string(), text_elements: Vec::new(), @@ -368,11 +372,13 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { ..Default::default() }) .await?; - timeout( + let running_turn_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, - primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)), + primary.read_stream_until_response_message(RequestId::Integer(running_turn_request_id)), ) .await??; + let TurnStartResponse { turn: running_turn } = + to_response::(running_turn_resp)?; timeout( DEFAULT_READ_TIMEOUT, primary.read_stream_until_notification_message("turn/started"), @@ -381,7 +387,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { let resume_id = primary .send_thread_resume_request(ThreadResumeParams { - thread_id: thread.id, + thread_id: thread_id.clone(), history: Some(vec![ResponseItem::Message { id: None, role: "user".to_string(), @@ -407,6 +413,26 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { resume_err.error.message ); + let interrupt_id = primary + .send_turn_interrupt_request(TurnInterruptParams { + thread_id, + turn_id: running_turn.id, + }) + .await?; + let interrupt_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(interrupt_id)), + ) + .await??; + let _turn_interrupt_response: TurnInterruptResponse = + to_response::(interrupt_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_notification_message("codex/event/turn_aborted"), + ) + .await??; + Ok(()) } @@ -462,9 +488,10 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul .await??; primary.clear_message_buffer(); - let running_turn_id = primary + let thread_id = thread.id.clone(); + let running_turn_request_id = primary .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), + thread_id: thread_id.clone(), input: vec![UserInput::Text { text: "keep running".to_string(), text_elements: Vec::new(), @@ -472,11 +499,13 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul ..Default::default() }) .await?; - timeout( + let running_turn_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, - primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)), + primary.read_stream_until_response_message(RequestId::Integer(running_turn_request_id)), ) .await??; + let TurnStartResponse { turn: running_turn } = + to_response::(running_turn_resp)?; timeout( DEFAULT_READ_TIMEOUT, primary.read_stream_until_notification_message("turn/started"), @@ -485,7 +514,7 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul let resume_id = primary .send_thread_resume_request(ThreadResumeParams { - thread_id: thread.id, + thread_id: thread_id.clone(), path: Some(PathBuf::from("/tmp/does-not-match-running-rollout.jsonl")), ..Default::default() }) @@ -501,6 +530,26 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul resume_err.error.message ); + let interrupt_id = primary + .send_turn_interrupt_request(TurnInterruptParams { + thread_id, + turn_id: running_turn.id, + }) + .await?; + let interrupt_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(interrupt_id)), + ) + .await??; + let _turn_interrupt_response: TurnInterruptResponse = + to_response::(interrupt_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_notification_message("codex/event/turn_aborted"), + ) + .await??; + Ok(()) } diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index c79bb00bc9..a4be91046c 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -250,6 +250,9 @@ impl Features { pub fn emit_metrics(&self, otel: &OtelManager) { for feature in FEATURES { + if matches!(feature.stage, Stage::Removed) { + continue; + } if self.enabled(feature.id) != feature.default_enabled { otel.counter( "codex.feature.state", diff --git a/codex-rs/core/templates/collaboration_mode/plan.md b/codex-rs/core/templates/collaboration_mode/plan.md index adde92745a..5c58310ae4 100644 --- a/codex-rs/core/templates/collaboration_mode/plan.md +++ b/codex-rs/core/templates/collaboration_mode/plan.md @@ -118,3 +118,5 @@ plan content should be human and agent digestible. The final plan must be plan-o Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan. Only produce at most one `` block per turn, and only when you are presenting a complete spec. + +If the user stays in Plan mode and asks for revisions after a prior ``, any new `` must be a complete replacement. diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index d1394f7a74..4268077338 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -912,6 +912,11 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { let fixture = test_codex() .with_config(move |config| { + // This test seeds OAuth tokens in CODEX_HOME/.credentials.json and + // validates file-backed OAuth loading. Force file mode so Linux + // keyring backend quirks do not affect this test. + config.mcp_oauth_credentials_store_mode = serde_json::from_value(json!("file")) + .expect("`file` should deserialize as OAuthCredentialsStoreMode"); let mut servers = config.mcp_servers.get().clone(); servers.insert( server_name.to_string(),