mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +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
@@ -17,15 +17,12 @@ use crate::session::turn_input;
|
||||
use crate::config::Config;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::GuardianApprovedAction;
|
||||
use crate::context::NodeReplReviewEvidence;
|
||||
use crate::review_prompts::resolve_review_request;
|
||||
use crate::session::spawn_review_thread;
|
||||
use crate::state::ReasoningEffortPin;
|
||||
use crate::tasks::CompactTask;
|
||||
use crate::tasks::UserShellCommandMode;
|
||||
use crate::tasks::UserShellCommandTask;
|
||||
use crate::tasks::execute_user_shell_command;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::Event;
|
||||
@@ -39,7 +36,6 @@ use codex_protocol::protocol::RealtimeVoicesList;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
@@ -252,129 +248,6 @@ pub async fn compact(sess: &Arc<Session>, sub_id: String) {
|
||||
sess.spawn_task(turn_context, Vec::new(), CompactTask).await;
|
||||
}
|
||||
|
||||
pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32) {
|
||||
if num_turns == 0 {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "num_turns must be >= 1".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let has_active_turn = { sess.active_turn.lock().await.is_some() };
|
||||
if has_active_turn {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Cannot rollback while a turn is in progress.".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let turn_context = sess
|
||||
.new_turn_with_default_settings(sub_id, Default::default())
|
||||
.await;
|
||||
let live_thread = match sess.live_thread_for_persistence("rollback thread") {
|
||||
Ok(live_thread) => live_thread,
|
||||
Err(_) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "thread rollback requires persisted thread history".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = live_thread.flush().await {
|
||||
sess.send_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: format!("failed to flush thread persistence for rollback replay: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let stored_history = match live_thread.load_history(/*include_archived*/ false).await {
|
||||
Ok(history) => history,
|
||||
Err(err) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: format!("failed to load thread history for rollback replay: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let rollback_event = ThreadRolledBackEvent { num_turns };
|
||||
let rollback_msg = EventMsg::ThreadRolledBack(rollback_event.clone());
|
||||
let replay_items = stored_history
|
||||
.items
|
||||
.into_iter()
|
||||
.chain(std::iter::once(RolloutItem::EventMsg(rollback_msg.clone())))
|
||||
.collect::<Vec<_>>();
|
||||
sess.apply_rollout_reconstruction(turn_context.as_ref(), replay_items.as_slice())
|
||||
.await;
|
||||
{
|
||||
let mut state = sess.state.lock().await;
|
||||
// Keep the baseline while startup prewarm is retained for the first turn,
|
||||
// including when its task has not established the pin yet.
|
||||
if state.startup_prewarm.is_none() {
|
||||
state.reasoning_effort_pin = ReasoningEffortPin::Unset;
|
||||
}
|
||||
}
|
||||
sess.services
|
||||
.thread_extension_data
|
||||
.remove::<NodeReplReviewEvidence>();
|
||||
sess.guardian_review_session().invalidate().await;
|
||||
sess.services
|
||||
.agent_control
|
||||
.rollout_budget()
|
||||
.rearm_reminder(sess.thread_id());
|
||||
sess.recompute_token_usage(turn_context.as_ref()).await;
|
||||
|
||||
sess.persist_rollout_items(&[RolloutItem::EventMsg(rollback_msg.clone())])
|
||||
.await;
|
||||
if let Err(err) = sess.flush_rollout().await {
|
||||
sess.send_event(
|
||||
turn_context.as_ref(),
|
||||
EventMsg::Warning(WarningEvent {
|
||||
message: format!(
|
||||
"Rolled the thread back, but failed to save the rollback marker. Codex will continue retrying. Error: {err}"
|
||||
),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
sess.deliver_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: rollback_msg,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(super) async fn persist_thread_memory_mode_update(
|
||||
sess: &Arc<Session>,
|
||||
mode: ThreadMemoryMode,
|
||||
@@ -689,10 +562,6 @@ pub(super) async fn submission_loop(
|
||||
compact(&sess, sub.id.clone()).await;
|
||||
false
|
||||
}
|
||||
Op::ThreadRollback { num_turns } => {
|
||||
thread_rollback(&sess, sub.id.clone(), num_turns).await;
|
||||
false
|
||||
}
|
||||
Op::SetThreadMemoryMode { mode } => {
|
||||
set_thread_memory_mode(&sess, sub.id.clone(), mode).await;
|
||||
false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Cache-preserving effort updates and the request-effort baseline for a context window.
|
||||
//!
|
||||
//! Only trusted harness items establish overrides. Replay and rollback preserve startup
|
||||
//! prewarm's baseline while it is retained; later rollback invalidates the runtime pin.
|
||||
//! Only trusted harness items establish overrides. Replay preserves startup prewarm's
|
||||
//! baseline while it is retained.
|
||||
//! Successful compaction retires the overrides and allows a fresh request baseline.
|
||||
|
||||
use super::session::Session;
|
||||
|
||||
@@ -86,7 +86,6 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
@@ -165,7 +164,6 @@ use codex_protocol::protocol::RealtimeVoicesList;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::Submission;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
@@ -3906,608 +3904,6 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_drops_last_turn_from_history() {
|
||||
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let rollout_path = attach_thread_persistence(
|
||||
Arc::get_mut(&mut sess).expect("session should not have additional references"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
let turn_1 = vec![
|
||||
user_message("turn 1 user"),
|
||||
assistant_message("turn 1 assistant"),
|
||||
];
|
||||
let turn_2 = vec![
|
||||
user_message("turn 2 user"),
|
||||
assistant_message("turn 2 assistant"),
|
||||
];
|
||||
let mut full_history = Vec::new();
|
||||
full_history.extend(initial_context.clone());
|
||||
full_history.extend(turn_1.clone());
|
||||
full_history.extend(turn_2);
|
||||
sess.replace_history(full_history.clone(), Some(tc.to_turn_context_item()))
|
||||
.await;
|
||||
let rollout_items: Vec<RolloutItem> = full_history
|
||||
.into_iter()
|
||||
.map(ResponseItemEnvelope::new)
|
||||
.map(RolloutItem::ResponseItem)
|
||||
.collect();
|
||||
sess.persist_rollout_items(&rollout_items).await;
|
||||
sess.set_previous_turn_settings(Some(PreviousTurnSettings {
|
||||
model: "stale-model".to_string(),
|
||||
comp_hash: None,
|
||||
realtime_active: Some(tc.realtime_active),
|
||||
}))
|
||||
.await;
|
||||
{
|
||||
let mut state = sess.state.lock().await;
|
||||
state.set_reference_context_item(Some(tc.to_turn_context_item()));
|
||||
}
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
|
||||
let rollback_event = wait_for_thread_rolled_back(&rx).await;
|
||||
assert_eq!(rollback_event.num_turns, 1);
|
||||
|
||||
let mut expected = Vec::new();
|
||||
expected.extend(initial_context);
|
||||
expected.extend(turn_1);
|
||||
|
||||
let history = sess.clone_history().await;
|
||||
assert_eq!(expected, raw_history_items(&history));
|
||||
assert_eq!(sess.previous_turn_settings().await, None);
|
||||
assert!(sess.reference_context_item().await.is_none());
|
||||
|
||||
let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path)
|
||||
.await
|
||||
.expect("read rollout history")
|
||||
else {
|
||||
panic!("expected resumed rollout history");
|
||||
};
|
||||
assert!(resumed.history.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(rollback))
|
||||
if rollback.num_turns == 1
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns() {
|
||||
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
attach_thread_persistence(
|
||||
Arc::get_mut(&mut sess).expect("session should not have additional references"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
let turn_1 = vec![user_message("turn 1 user")];
|
||||
let mut full_history = Vec::new();
|
||||
full_history.extend(initial_context.clone());
|
||||
full_history.extend(turn_1);
|
||||
sess.replace_history(full_history.clone(), Some(tc.to_turn_context_item()))
|
||||
.await;
|
||||
let rollout_items: Vec<RolloutItem> = full_history
|
||||
.into_iter()
|
||||
.map(ResponseItemEnvelope::new)
|
||||
.map(RolloutItem::ResponseItem)
|
||||
.collect();
|
||||
sess.persist_rollout_items(&rollout_items).await;
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 99).await;
|
||||
|
||||
let rollback_event = wait_for_thread_rolled_back(&rx).await;
|
||||
assert_eq!(rollback_event.num_turns, 99);
|
||||
|
||||
let history = sess.clone_history().await;
|
||||
assert_eq!(initial_context, raw_history_items(&history));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_fails_without_persisted_thread_history() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context)
|
||||
.await;
|
||||
let history_before_rollback = sess.clone_history().await;
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
|
||||
let error_event = wait_for_thread_rollback_failed(&rx).await;
|
||||
assert_eq!(
|
||||
error_event.message,
|
||||
"thread rollback requires persisted thread history"
|
||||
);
|
||||
assert_eq!(
|
||||
error_event.codex_error_info,
|
||||
Some(CodexErrorInfo::ThreadRollbackFailed)
|
||||
);
|
||||
assert_eq!(
|
||||
raw_history_items(&sess.clone_history().await),
|
||||
raw_history_items(&history_before_rollback)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context_from_replay() {
|
||||
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
attach_thread_persistence(
|
||||
Arc::get_mut(&mut sess).expect("session should not have additional references"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let first_context_item = tc.to_turn_context_item();
|
||||
let first_turn_id = first_context_item
|
||||
.turn_id
|
||||
.clone()
|
||||
.expect("thread settings should have turn_id");
|
||||
let mut rolled_back_context_item = first_context_item.clone();
|
||||
rolled_back_context_item.turn_id = Some("rolled-back-turn".to_string());
|
||||
rolled_back_context_item.model = "rolled-back-model".to_string();
|
||||
let rolled_back_turn_id = rolled_back_context_item
|
||||
.turn_id
|
||||
.clone()
|
||||
.expect("thread settings should have turn_id");
|
||||
let turn_one_user = user_message("turn 1 user");
|
||||
let turn_one_assistant = assistant_message("turn 1 assistant");
|
||||
let turn_two_user = user_message("turn 2 user");
|
||||
let turn_two_assistant = assistant_message("turn 2 assistant");
|
||||
|
||||
sess.persist_rollout_items(&[
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(
|
||||
codex_protocol::protocol::UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 1 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
RolloutItem::TurnContext(first_context_item.clone()),
|
||||
RolloutItem::ResponseItem(turn_one_user.clone().into()),
|
||||
RolloutItem::ResponseItem(turn_one_assistant.clone().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: first_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: rolled_back_turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(
|
||||
codex_protocol::protocol::UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 2 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
RolloutItem::TurnContext(rolled_back_context_item),
|
||||
RolloutItem::ResponseItem(turn_two_user.into()),
|
||||
RolloutItem::ResponseItem(turn_two_assistant.into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: rolled_back_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
])
|
||||
.await;
|
||||
sess.replace_history(
|
||||
vec![assistant_message("stale history")],
|
||||
Some(first_context_item.clone()),
|
||||
)
|
||||
.await;
|
||||
sess.set_previous_turn_settings(Some(PreviousTurnSettings {
|
||||
model: "stale-model".to_string(),
|
||||
comp_hash: None,
|
||||
realtime_active: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
let rollback_event = wait_for_thread_rolled_back(&rx).await;
|
||||
assert_eq!(rollback_event.num_turns, 1);
|
||||
|
||||
assert_eq!(
|
||||
raw_history_items(&sess.clone_history().await),
|
||||
vec![turn_one_user, turn_one_assistant]
|
||||
);
|
||||
assert_eq!(
|
||||
sess.previous_turn_settings().await,
|
||||
Some(PreviousTurnSettings {
|
||||
model: tc.model_info().slug.clone(),
|
||||
comp_hash: None,
|
||||
realtime_active: Some(tc.realtime_active),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(sess.reference_context_item().await)
|
||||
.expect("serialize replay reference context item"),
|
||||
serde_json::to_value(Some(first_context_item))
|
||||
.expect("serialize expected reference context item")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_restores_cleared_reference_context_item_after_compaction() {
|
||||
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
attach_thread_persistence(
|
||||
Arc::get_mut(&mut sess).expect("session should not have additional references"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let first_context_item = tc.to_turn_context_item();
|
||||
let first_turn_id = first_context_item
|
||||
.turn_id
|
||||
.clone()
|
||||
.expect("thread settings should have turn_id");
|
||||
let compact_turn_id = "compact-turn".to_string();
|
||||
let rolled_back_turn_id = "rolled-back-turn".to_string();
|
||||
let compacted_history = vec![
|
||||
user_message("turn 1 user"),
|
||||
user_message("summary after compaction"),
|
||||
];
|
||||
let first_window_id = Uuid::now_v7();
|
||||
let previous_window_id = Uuid::now_v7();
|
||||
let compacted_window_id = Uuid::now_v7();
|
||||
|
||||
sess.persist_rollout_items(&[
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 1 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::TurnContext(first_context_item.clone()),
|
||||
RolloutItem::ResponseItem(user_message("turn 1 user").into()),
|
||||
RolloutItem::ResponseItem(assistant_message("turn 1 assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: first_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: compact_turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: "summary after compaction".to_string(),
|
||||
replacement_history: Some(
|
||||
compacted_history
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(ResponseItemEnvelope::new)
|
||||
.collect(),
|
||||
),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(7),
|
||||
first_window_id: Some(first_window_id.to_string()),
|
||||
previous_window_id: Some(previous_window_id.to_string()),
|
||||
window_id: Some(compacted_window_id.to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: compact_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: rolled_back_turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 2 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: Some(rolled_back_turn_id.clone()),
|
||||
model: "rolled-back-model".to_string(),
|
||||
comp_hash: None,
|
||||
..first_context_item.clone()
|
||||
}),
|
||||
RolloutItem::ResponseItem(user_message("turn 2 user").into()),
|
||||
RolloutItem::ResponseItem(assistant_message("turn 2 assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: rolled_back_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
])
|
||||
.await;
|
||||
sess.replace_history(
|
||||
vec![assistant_message("stale history")],
|
||||
Some(first_context_item),
|
||||
)
|
||||
.await;
|
||||
{
|
||||
let mut state = sess.state.lock().await;
|
||||
state.restore_auto_compact_window(
|
||||
/*window_number*/ 99,
|
||||
AutoCompactWindowIds {
|
||||
first_window_id: Uuid::now_v7(),
|
||||
previous_window_id: Some(Uuid::now_v7()),
|
||||
window_id: Uuid::now_v7(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
let rollback_event = wait_for_thread_rolled_back(&rx).await;
|
||||
assert_eq!(rollback_event.num_turns, 1);
|
||||
|
||||
assert_eq!(
|
||||
raw_history_items(&sess.clone_history().await),
|
||||
compacted_history
|
||||
);
|
||||
assert!(sess.reference_context_item().await.is_none());
|
||||
assert_eq!(
|
||||
sess.state.lock().await.auto_compact_window_ids(),
|
||||
AutoCompactWindowIds {
|
||||
first_window_id,
|
||||
previous_window_id: Some(previous_window_id),
|
||||
window_id: compacted_window_id,
|
||||
}
|
||||
);
|
||||
assert!(sess.current_window_id().await.ends_with(":7"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_persists_marker_and_replays_cumulatively() {
|
||||
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let rollout_path = attach_thread_persistence(
|
||||
Arc::get_mut(&mut sess).expect("session should not have additional references"),
|
||||
)
|
||||
.await;
|
||||
let turn_context_item = tc.to_turn_context_item();
|
||||
|
||||
sess.persist_rollout_items(&[
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 1 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::TurnContext(turn_context_item.clone()),
|
||||
RolloutItem::ResponseItem(user_message("turn 1 user").into()),
|
||||
RolloutItem::ResponseItem(assistant_message("turn 1 assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-2".to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 2 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::TurnContext(turn_context_item.clone()),
|
||||
RolloutItem::ResponseItem(user_message("turn 2 user").into()),
|
||||
RolloutItem::ResponseItem(assistant_message("turn 2 assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-3".to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
},
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "turn 3 user".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::TurnContext(turn_context_item),
|
||||
RolloutItem::ResponseItem(user_message("turn 3 user").into()),
|
||||
RolloutItem::ResponseItem(assistant_message("turn 3 assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-3".to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
])
|
||||
.await;
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
let first_rollback = wait_for_thread_rolled_back(&rx).await;
|
||||
assert_eq!(first_rollback.num_turns, 1);
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
let second_rollback = wait_for_thread_rolled_back(&rx).await;
|
||||
assert_eq!(second_rollback.num_turns, 1);
|
||||
|
||||
assert_eq!(
|
||||
raw_history_items(&sess.clone_history().await),
|
||||
vec![
|
||||
user_message("turn 1 user"),
|
||||
assistant_message("turn 1 assistant")
|
||||
]
|
||||
);
|
||||
|
||||
let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path)
|
||||
.await
|
||||
.expect("read rollout history")
|
||||
else {
|
||||
panic!("expected resumed rollout history");
|
||||
};
|
||||
let rollback_markers = resumed
|
||||
.history
|
||||
.iter()
|
||||
.filter(|item| matches!(item, RolloutItem::EventMsg(EventMsg::ThreadRolledBack(_))))
|
||||
.count();
|
||||
assert_eq!(rollback_markers, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_fails_when_turn_in_progress() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context)
|
||||
.await;
|
||||
let history_before_rollback = sess.clone_history().await;
|
||||
|
||||
*sess.active_turn.lock().await = Some(crate::state::ActiveTurn::default());
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||
|
||||
let error_event = wait_for_thread_rollback_failed(&rx).await;
|
||||
assert_eq!(
|
||||
error_event.codex_error_info,
|
||||
Some(CodexErrorInfo::ThreadRollbackFailed)
|
||||
);
|
||||
|
||||
let history = sess.clone_history().await;
|
||||
assert_eq!(
|
||||
raw_history_items(&history_before_rollback),
|
||||
raw_history_items(&history)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_rollback_fails_when_num_turns_is_zero() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context)
|
||||
.await;
|
||||
let history_before_rollback = sess.clone_history().await;
|
||||
|
||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 0).await;
|
||||
|
||||
let error_event = wait_for_thread_rollback_failed(&rx).await;
|
||||
assert_eq!(error_event.message, "num_turns must be >= 1");
|
||||
assert_eq!(
|
||||
error_event.codex_error_info,
|
||||
Some(CodexErrorInfo::ThreadRollbackFailed)
|
||||
);
|
||||
|
||||
let history = sess.clone_history().await;
|
||||
assert_eq!(
|
||||
raw_history_items(&history_before_rollback),
|
||||
raw_history_items(&history)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_rate_limits_retains_previous_credits() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
@@ -4965,42 +4361,6 @@ fn success_flag_true_with_no_error_and_content_used() {
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
|
||||
async fn wait_for_thread_rolled_back(rx: &async_channel::Receiver<Event>) -> ThreadRolledBackEvent {
|
||||
let deadline = StdDuration::from_secs(2);
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
let remaining = deadline.saturating_sub(start.elapsed());
|
||||
let evt = tokio::time::timeout(remaining, rx.recv())
|
||||
.await
|
||||
.expect("timeout waiting for event")
|
||||
.expect("event");
|
||||
match evt.msg {
|
||||
EventMsg::ThreadRolledBack(payload) => return payload,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_thread_rollback_failed(rx: &async_channel::Receiver<Event>) -> ErrorEvent {
|
||||
let deadline = StdDuration::from_secs(2);
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
let remaining = deadline.saturating_sub(start.elapsed());
|
||||
let evt = tokio::time::timeout(remaining, rx.recv())
|
||||
.await
|
||||
.expect("timeout waiting for event")
|
||||
.expect("event");
|
||||
match evt.msg {
|
||||
EventMsg::Error(payload)
|
||||
if payload.codex_error_info == Some(CodexErrorInfo::ThreadRollbackFailed) =>
|
||||
{
|
||||
return payload;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_thread_persistence(session: &mut Session) -> PathBuf {
|
||||
let config = session.get_config().await;
|
||||
let live_thread = LiveThread::create(
|
||||
|
||||
@@ -503,7 +503,7 @@ pub(crate) async fn run_turn(
|
||||
.record_step_world_state_if_changed(&world_state, step_context.as_ref())
|
||||
.await?;
|
||||
|
||||
// Keep the override after accepted input so ordinary turn rollback removes it too.
|
||||
// Keep the override after accepted input so history truncation removes them together.
|
||||
sess.record_reasoning_effort_override(step_context.as_ref())
|
||||
.await;
|
||||
|
||||
|
||||
Reference in new issue
Block a user