mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Remove the deprecated thread/rollback API (#44915)
## What changed Remove `thread/rollback`, its request and response types, generated bindings, and the core `Op::ThreadRollback` operation. Requests now follow the generic unknown-method rejection path. Document `thread/revert` as the alternative for paginated threads. Keep historical `ThreadRolledBack` markers and legacy error deserialization so existing rollouts remain compatible with replay and migration. ## Testing Adapt retained-context and Guardian history tests to append legacy rollback markers and resume threads, preserving coverage of surviving instructions, answers, and review history. GitOrigin-RevId: b3da1becdf86b1869275aacb0ffc2817cee5af2e
This commit is contained in:
1 parent
c62d191c4c
commit
3052bbcf8c
65 files changed
+124
-5472
No files matched your search
@@ -97,10 +97,6 @@ async fn websocket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Resu
|
||||
"turn/settings/update",
|
||||
json!({"threadId": thread_id, "turnId": "unused", "model": "other"}),
|
||||
),
|
||||
(
|
||||
"thread/rollback",
|
||||
json!({"threadId": thread_id, "numTurns": 1}),
|
||||
),
|
||||
(
|
||||
"thread/revert",
|
||||
json!({"threadId": thread_id, "beforeTurnId": "unused"}),
|
||||
|
||||
@@ -17,8 +17,6 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -79,7 +77,7 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()>
|
||||
"Recovered",
|
||||
"Cached",
|
||||
"After switch",
|
||||
"After rollback",
|
||||
"After switching back",
|
||||
]
|
||||
.into_iter()
|
||||
.map(create_final_assistant_message_sse_response)
|
||||
@@ -166,14 +164,6 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()>
|
||||
let _: LoginAccountResponse = read_response(&mut app_server, request_id).await?;
|
||||
run_turn(&mut app_server, &thread.id, "Turn after workspace switch").await?;
|
||||
|
||||
let request_id = app_server
|
||||
.send_thread_rollback_request(ThreadRollbackParams {
|
||||
thread_id: thread.id.clone(),
|
||||
num_turns: 1,
|
||||
})
|
||||
.await?;
|
||||
let _: ThreadRollbackResponse = read_response(&mut app_server, request_id).await?;
|
||||
|
||||
let request_id = app_server
|
||||
.send_chatgpt_auth_tokens_login_request(
|
||||
"e30.e30.c2ln".to_string(),
|
||||
@@ -182,13 +172,13 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()>
|
||||
)
|
||||
.await?;
|
||||
let _: LoginAccountResponse = read_response(&mut app_server, request_id).await?;
|
||||
run_turn(&mut app_server, &thread.id, "Turn after rollback").await?;
|
||||
run_turn(&mut app_server, &thread.id, "Turn after switching back").await?;
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 5);
|
||||
for (request, expected) in requests
|
||||
.into_iter()
|
||||
.zip([(0, 0), (1, 0), (1, 0), (1, 1), (1, 0)])
|
||||
.zip([(0, 0), (1, 0), (1, 0), (1, 1), (2, 1)])
|
||||
{
|
||||
let developer_text = request.message_input_texts("developer").join("\n");
|
||||
assert_eq!(
|
||||
|
||||
@@ -36,12 +36,9 @@ use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
use codex_app_server_protocol::ThreadCompactStartResponse;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateParams;
|
||||
@@ -247,7 +244,6 @@ enum ThreadLifecycle {
|
||||
UserInputHookBlocked,
|
||||
Resume,
|
||||
Fork,
|
||||
RootRollback,
|
||||
RootRestriction,
|
||||
RootRestrictionDuringClassification,
|
||||
RootTrustedSkill,
|
||||
@@ -261,8 +257,7 @@ impl ThreadLifecycle {
|
||||
fn uses_root_worker(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::RootRollback
|
||||
| Self::RootRestriction
|
||||
Self::RootRestriction
|
||||
| Self::RootRestrictionDuringClassification
|
||||
| Self::RootTrustedSkill
|
||||
| Self::RootUserInputRestriction
|
||||
@@ -914,7 +909,6 @@ async fn guardian_v2_routes_scoped_tool_approvals(
|
||||
| ThreadLifecycle::UserInputEmpty
|
||||
| ThreadLifecycle::UserInputHookFeedback
|
||||
| ThreadLifecycle::UserInputHookBlocked
|
||||
| ThreadLifecycle::RootRollback
|
||||
| ThreadLifecycle::RootRestriction
|
||||
| ThreadLifecycle::RootRestrictionDuringClassification
|
||||
| ThreadLifecycle::RootTrustedSkill
|
||||
@@ -963,7 +957,6 @@ async fn guardian_v2_routes_scoped_tool_approvals(
|
||||
| ThreadLifecycle::UserInputEmpty
|
||||
| ThreadLifecycle::UserInputHookFeedback
|
||||
| ThreadLifecycle::UserInputHookBlocked
|
||||
| ThreadLifecycle::RootRollback
|
||||
| ThreadLifecycle::RootRestriction
|
||||
| ThreadLifecycle::RootRestrictionDuringClassification
|
||||
| ThreadLifecycle::RootTrustedSkill
|
||||
@@ -975,8 +968,6 @@ async fn guardian_v2_routes_scoped_tool_approvals(
|
||||
.start_thread(ThreadStartParams {
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
approvals_reviewer: Some(requested_reviewer),
|
||||
history_mode: matches!(lifecycle, ThreadLifecycle::RootRollback)
|
||||
.then_some(ThreadHistoryMode::Legacy),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -1560,17 +1551,6 @@ async fn guardian_v2_routes_scoped_tool_approvals(
|
||||
&& (lifecycle.uses_root_worker()
|
||||
|| matches!(lifecycle, ThreadLifecycle::RootUserRestriction))
|
||||
{
|
||||
if matches!(lifecycle, ThreadLifecycle::RootRollback) {
|
||||
let rollback_id = app_server
|
||||
.send_thread_rollback_request(ThreadRollbackParams {
|
||||
thread_id: thread_id.clone(),
|
||||
num_turns: 1,
|
||||
})
|
||||
.await?;
|
||||
let _: ThreadRollbackResponse =
|
||||
timeout(TIMEOUT, app_server.read_response(rollback_id)).await??;
|
||||
}
|
||||
|
||||
if lifecycle.has_root_user_input() {
|
||||
submit_user_input_response(
|
||||
&mut app_server,
|
||||
@@ -2548,7 +2528,6 @@ async fn forked_thread_ignores_persisted_guardian_score() -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[test_case(ThreadLifecycle::RootRollback; "worker_root_rollback")]
|
||||
#[test_case(ThreadLifecycle::RootRestriction; "worker_root_restriction")]
|
||||
#[test_case(ThreadLifecycle::RootUserRestriction; "root_user_restriction")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Exercises both reviewers' evidence delivery through real compaction and rollback.
|
||||
//! Exercises both reviewers' evidence delivery through real compaction and resume.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -24,8 +24,6 @@ use codex_app_server_protocol::ThreadCompactStartResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -108,7 +106,7 @@ enum ReviewCheckpoint {
|
||||
#[test_case(ContextPath::Legacy, CheckpointReuse::Enabled, Some("matching"), Some("matching"), 140, EvidenceSize::Normal, ReviewCheckpoint::Valid; "legacy source call evicted")]
|
||||
#[test_case(ContextPath::Legacy, CheckpointReuse::Enabled, Some("matching"), Some("matching"), 0, EvidenceSize::OversizedAnswer, ReviewCheckpoint::Valid; "legacy answer truncation")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollback(
|
||||
async fn guardians_retain_evidence_after_compaction_and_resume(
|
||||
context_path: ContextPath,
|
||||
checkpoint_reuse: CheckpointReuse,
|
||||
parent_hash: Option<&str>,
|
||||
@@ -334,8 +332,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
restriction.as_str(),
|
||||
"Recheck the repository.",
|
||||
"Inspect after resume.",
|
||||
"Inspect after partial rollback.",
|
||||
"Inspect a different repository.",
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -368,15 +364,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
timeout(TIMEOUT, app_server.read_notification("turn/completed")).await??;
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
assert_eq!(compact_requests.lock().expect("request log lock").len(), 1);
|
||||
} else if index >= 3 {
|
||||
let id = app_server
|
||||
.send_thread_rollback_request(ThreadRollbackParams {
|
||||
thread_id: thread_id.clone(),
|
||||
num_turns: if index == 3 { 1 } else { 3 },
|
||||
})
|
||||
.await?;
|
||||
let _: ThreadRollbackResponse =
|
||||
timeout(TIMEOUT, app_server.read_response(id)).await??;
|
||||
}
|
||||
|
||||
let id = app_server
|
||||
@@ -492,7 +479,7 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if requires_sync && (1..=3).contains(&index) {
|
||||
if requires_sync && index > 0 {
|
||||
let reviews = review_requests.lock().expect("request log lock");
|
||||
assert_eq!(
|
||||
reviews.len(),
|
||||
@@ -513,12 +500,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
"current user input missing from sync review: {text}"
|
||||
);
|
||||
}
|
||||
if index == 3 {
|
||||
assert!(
|
||||
!text.contains("Inspect after resume."),
|
||||
"rolled-back user input remains in sync review: {text}"
|
||||
);
|
||||
}
|
||||
assert!(text.contains(USER_INPUT_RESTRICTION));
|
||||
assert!(text.contains("TRUSTED USER ANSWERS START"));
|
||||
assert!(text.contains("TRANSCRIPT START"));
|
||||
@@ -575,11 +556,8 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
let review = &reviews[index];
|
||||
let sync_input = review["input"].as_array().expect("request input array");
|
||||
let async_input = request["input"].as_array().expect("request input array");
|
||||
assert_eq!(sync_input.contains(&checkpoint), (1..=3).contains(&index));
|
||||
assert_eq!(
|
||||
async_input.contains(&checkpoint),
|
||||
(1..=3).contains(&index) && compatible
|
||||
);
|
||||
assert_eq!(sync_input.contains(&checkpoint), index > 0);
|
||||
assert_eq!(async_input.contains(&checkpoint), index > 0 && compatible);
|
||||
let sync_text = sync_input
|
||||
.iter()
|
||||
.filter(|item| item["role"] == "user")
|
||||
@@ -591,14 +569,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
sync_text.contains(prompt),
|
||||
"current user input missing from sync review: {sync_text}"
|
||||
);
|
||||
if index == 3 {
|
||||
for (consumer, text) in [("sync", sync_text.as_str()), ("async", transcript)] {
|
||||
assert!(
|
||||
!text.contains("Inspect after resume."),
|
||||
"rolled-back user input remains in {consumer} review: {text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if index == 0 {
|
||||
let input = parent[2]["input"].as_array().expect("request input array");
|
||||
let output = input
|
||||
@@ -615,12 +585,12 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
.expect("request input array");
|
||||
let parent_input = serde_json::to_string(parent_items)?;
|
||||
// V2 keeps bounded user history while replacing old tool output with a checkpoint.
|
||||
assert_eq!(parent_input.contains(RESTRICTION), index <= 3);
|
||||
assert!(parent_input.contains(RESTRICTION));
|
||||
assert!(!parent_input.contains(EVIDENCE));
|
||||
assert!(!parent_items.iter().any(|item| {
|
||||
item["type"] == "function_call_output" && item["call_id"] == "inspect-1"
|
||||
}));
|
||||
if index <= 3 {
|
||||
{
|
||||
assert!(sync_text.contains(RESTRICTION));
|
||||
if matches!(context_path, ContextPath::ThreadOwned) {
|
||||
assert!(
|
||||
@@ -678,17 +648,10 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
assert!(!transcript.contains("tool request_user_input call:"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert!(!sync_text.contains(RESTRICTION));
|
||||
assert!(!sync_text.contains(EVIDENCE));
|
||||
assert!(!transcript.contains(RESTRICTION));
|
||||
assert!(!transcript.contains(EVIDENCE));
|
||||
assert!(!transcript.contains("Recheck the repository."));
|
||||
assert!(!transcript.contains("\"echoed\":\"current inspection\""));
|
||||
}
|
||||
}
|
||||
for (consumer, text) in [("async", &content), ("sync", &sync_text)] {
|
||||
if index < 4 && (matches!(context_path, ContextPath::ThreadOwned) || index == 0) {
|
||||
if matches!(context_path, ContextPath::ThreadOwned) || index == 0 {
|
||||
let answers = text
|
||||
.split_once(">>> TRUSTED USER ANSWERS START")
|
||||
.unwrap_or_else(|| {
|
||||
@@ -715,9 +678,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if index == 4 {
|
||||
assert!(!text.contains(USER_INPUT_RESTRICTION));
|
||||
}
|
||||
assert!(!text.contains(">>> TRUSTED USER ANSWERS START"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,6 @@ mod thread_queue;
|
||||
mod thread_read;
|
||||
mod thread_resume;
|
||||
mod thread_revert;
|
||||
mod thread_rollback;
|
||||
mod thread_sections;
|
||||
mod thread_settings_update;
|
||||
mod thread_shell_command;
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::MockResponsesConfig;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::DeprecationNoticeNotification;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_rejects_paginated_thread() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
history_mode: Some(ThreadHistoryMode::Paginated),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(start_resp)?;
|
||||
|
||||
let rollback_id = mcp
|
||||
.send_thread_rollback_request(ThreadRollbackParams {
|
||||
thread_id: thread.id,
|
||||
num_turns: 1,
|
||||
})
|
||||
.await?;
|
||||
let rollback_err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(rollback_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(rollback_err.error.code, -32600);
|
||||
assert_eq!(
|
||||
rollback_err.error.message,
|
||||
"paginated threads do not support thread/rollback"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_does_not_emit_deprecation_notice_to_codex_tui() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
let initialized = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_client_info(ClientInfo {
|
||||
name: "codex-tui".to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
let JSONRPCMessage::Response(_) = initialized else {
|
||||
panic!("expected initialize response, got {initialized:?}");
|
||||
};
|
||||
mcp.clear_message_buffer();
|
||||
|
||||
let rollback_id = mcp
|
||||
.send_thread_rollback_request(ThreadRollbackParams {
|
||||
thread_id: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
num_turns: 1,
|
||||
})
|
||||
.await?;
|
||||
loop {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
match message {
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
assert_ne!(notification.method, "deprecationNotice");
|
||||
}
|
||||
JSONRPCMessage::Error(error) if error.id == RequestId::Integer(rollback_id) => {
|
||||
break;
|
||||
}
|
||||
message => {
|
||||
panic!("expected rollback error response, got {message:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()> {
|
||||
// Three Codex turns hit the mock model (session start + two turn/start calls).
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("Done")?,
|
||||
create_final_assistant_message_sse_response("Done")?,
|
||||
create_final_assistant_message_sse_response("Done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// Start a thread.
|
||||
let start_id = mcp
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
history_mode: Some(ThreadHistoryMode::Legacy),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
// Two turns.
|
||||
let first_text = "First";
|
||||
let turn1_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: first_text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let _turn1_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn1_id)),
|
||||
)
|
||||
.await??;
|
||||
let _completed1 = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let turn2_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Second".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let _turn2_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn2_id)),
|
||||
)
|
||||
.await??;
|
||||
let _completed2 = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
drop(mcp);
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id.clone(),
|
||||
model: Some("gpt-5.2".to_string()),
|
||||
config: Some([("model_reasoning_effort".to_string(), json!("high"))].into()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let ThreadResumeResponse { thread, .. } =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??;
|
||||
// Drain the resume snapshots before checking rollback notification ordering.
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/goal/cleared"),
|
||||
)
|
||||
.await??;
|
||||
mcp.clear_message_buffer();
|
||||
|
||||
// Roll back the last turn.
|
||||
let rollback_id = mcp
|
||||
.send_thread_rollback_request(ThreadRollbackParams {
|
||||
thread_id: thread.id.clone(),
|
||||
num_turns: 1,
|
||||
})
|
||||
.await?;
|
||||
// Pipeline resume while the listener may still be handling rollback. Waiting for
|
||||
// its response must not retain the thread-list permit needed by that listener.
|
||||
let pipelined_resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id.clone(),
|
||||
exclude_turns: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let deprecation_notice = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(deprecation_notice) = deprecation_notice else {
|
||||
panic!("thread/rollback should emit deprecationNotice before its response");
|
||||
};
|
||||
assert_eq!(deprecation_notice.method, "deprecationNotice");
|
||||
let deprecation_notice: DeprecationNoticeNotification = serde_json::from_value(
|
||||
deprecation_notice
|
||||
.params
|
||||
.expect("deprecationNotice params should be present"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
deprecation_notice,
|
||||
DeprecationNoticeNotification {
|
||||
summary: "thread/rollback is deprecated and will be removed soon".to_string(),
|
||||
details: None,
|
||||
}
|
||||
);
|
||||
let rollback_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(rollback_id)),
|
||||
)
|
||||
.await??;
|
||||
let rollback_result = rollback_resp.result.clone();
|
||||
let ThreadRollbackResponse {
|
||||
thread: rolled_back_thread,
|
||||
} = to_response::<ThreadRollbackResponse>(rollback_resp)?;
|
||||
|
||||
// Wire contract: thread title field is `name`, serialized as null when unset.
|
||||
let thread_json = rollback_result
|
||||
.get("thread")
|
||||
.and_then(Value::as_object)
|
||||
.expect("thread/rollback result.thread must be an object");
|
||||
assert_eq!(rolled_back_thread.name, None);
|
||||
assert_eq!(rolled_back_thread.session_id, thread.session_id);
|
||||
assert_eq!(
|
||||
thread_json.get("name"),
|
||||
Some(&Value::Null),
|
||||
"thread/rollback must serialize `name: null` when unset"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("sessionId").and_then(Value::as_str),
|
||||
Some(thread.session_id.as_str())
|
||||
);
|
||||
|
||||
assert_eq!(rolled_back_thread.turns.len(), 1);
|
||||
assert_eq!(rolled_back_thread.status, ThreadStatus::Idle);
|
||||
assert_eq!(
|
||||
(
|
||||
rolled_back_thread.model.as_deref(),
|
||||
rolled_back_thread.reasoning_effort
|
||||
),
|
||||
(Some("gpt-5.2"), Some(ReasoningEffort::High))
|
||||
);
|
||||
assert_eq!(rolled_back_thread.turns[0].items.len(), 2);
|
||||
match &rolled_back_thread.turns[0].items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![V2UserInput::Text {
|
||||
text: first_text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
|
||||
let _: ThreadResumeResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(pipelined_resume_id)).await??;
|
||||
|
||||
// Resume after rollback completes to verify the pruned history.
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resume_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
assert_eq!(thread.status, ThreadStatus::Idle);
|
||||
assert_eq!(thread.turns[0].items.len(), 2);
|
||||
match &thread.turns[0].items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![V2UserInput::Text {
|
||||
text: first_text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4594,7 +4594,6 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected(
|
||||
"turn/settings/update",
|
||||
json!({"turnId": "any-child-turn", "model": "gpt-5.4"}),
|
||||
),
|
||||
("thread/rollback", json!({"numTurns": 1})),
|
||||
("thread/revert", json!({"beforeTurnId": "any-child-turn"})),
|
||||
(
|
||||
"review/start",
|
||||
|
||||
Reference in new issue
Block a user