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:
Owen Lin authored and copyberry committed 2026-09-11 21:32:04 +00:00
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",