Compare commits

...

2 Commits

Author SHA1 Message Date
Ahmed Ibrahim
710eb7cf6f Apply clippy fixes 2026-01-26 01:00:26 -08:00
Ahmed Ibrahim
1aea5ff6c1 Add append-only clear context operation 2026-01-26 00:56:15 -08:00
18 changed files with 325 additions and 26 deletions

View File

@@ -99,6 +99,7 @@ pub enum CodexErrorInfo {
Unauthorized,
BadRequest,
ThreadRollbackFailed,
ContextClearFailed,
SandboxError,
/// The response SSE stream disconnected in the middle of a turn before completion.
ResponseStreamDisconnected {
@@ -130,6 +131,7 @@ impl From<CoreCodexErrorInfo> for CodexErrorInfo {
CoreCodexErrorInfo::Unauthorized => CodexErrorInfo::Unauthorized,
CoreCodexErrorInfo::BadRequest => CodexErrorInfo::BadRequest,
CoreCodexErrorInfo::ThreadRollbackFailed => CodexErrorInfo::ThreadRollbackFailed,
CoreCodexErrorInfo::ContextClearFailed => CodexErrorInfo::ContextClearFailed,
CoreCodexErrorInfo::SandboxError => CodexErrorInfo::SandboxError,
CoreCodexErrorInfo::ResponseStreamDisconnected { http_status_code } => {
CodexErrorInfo::ResponseStreamDisconnected { http_status_code }

View File

@@ -75,6 +75,7 @@ impl<'a> ChatRequestBuilder<'a> {
ResponseItem::WebSearchCall { .. } => {}
ResponseItem::GhostSnapshot { .. } => {}
ResponseItem::Compaction { .. } => {}
ResponseItem::ContextCleared => last_emitted_role = None,
}
}
@@ -282,6 +283,10 @@ impl<'a> ChatRequestBuilder<'a> {
ResponseItem::GhostSnapshot { .. } => {
continue;
}
ResponseItem::ContextCleared => {
last_assistant_text = None;
continue;
}
ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::Other

View File

@@ -1560,7 +1560,7 @@ impl Session {
if let Some(replacement) = &compacted.replacement_history {
history.replace(replacement.clone());
} else {
let user_messages = collect_user_messages(history.raw_items());
let user_messages = collect_user_messages(history.active_items());
let rebuilt = compact::build_compacted_history(
self.build_initial_context(turn_context).await,
&user_messages,
@@ -2184,6 +2184,9 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
Op::ThreadRollback { num_turns } => {
handlers::thread_rollback(&sess, sub.id.clone(), num_turns).await;
}
Op::ClearContext => {
handlers::clear_context(&sess, sub.id.clone()).await;
}
Op::RunUserShellCommand { command } => {
handlers::run_user_shell_command(
&sess,
@@ -2232,7 +2235,9 @@ mod handlers {
use crate::tasks::UndoTask;
use crate::tasks::UserShellCommandTask;
use codex_protocol::custom_prompts::CustomPrompt;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ContextClearedEvent;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
@@ -2672,6 +2677,40 @@ mod handlers {
.await;
}
pub async fn clear_context(sess: &Arc<Session>, sub_id: String) {
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 {
message: "Cannot clear context while a turn is in progress.".to_string(),
codex_error_info: Some(CodexErrorInfo::ContextClearFailed),
}),
})
.await;
return;
}
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
let mut items = Vec::with_capacity(initial_context.len() + 1);
items.push(ResponseItem::ContextCleared);
items.extend(initial_context);
sess.record_into_history(&items, turn_context.as_ref())
.await;
sess.persist_rollout_response_items(&items).await;
sess.send_event_raw_flushed(Event {
id: turn_context.sub_id.clone(),
msg: EventMsg::ContextCleared(ContextClearedEvent),
})
.await;
sess.recompute_token_usage(turn_context.as_ref()).await;
sess.send_raw_response_items(turn_context.as_ref(), &items)
.await;
}
pub async fn shutdown(sess: &Arc<Session>, sub_id: String) -> bool {
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
sess.services
@@ -3909,6 +3948,69 @@ mod tests {
assert_eq!(initial_context, history.raw_items());
}
#[tokio::test]
async fn clear_context_appends_marker_and_initial_context() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = sess.build_initial_context(tc.as_ref()).await;
sess.record_into_history(&initial_context, tc.as_ref())
.await;
let turn = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "turn user".to_string(),
}],
end_turn: None,
},
ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "turn assistant".to_string(),
}],
end_turn: None,
},
];
sess.record_into_history(&turn, tc.as_ref()).await;
handlers::clear_context(&sess, "sub-1".to_string()).await;
wait_for_context_cleared(&rx).await;
let mut expected = Vec::new();
expected.extend(initial_context.clone());
expected.extend(turn.clone());
expected.push(ResponseItem::ContextCleared);
expected.extend(initial_context.clone());
let history = sess.clone_history().await;
assert_eq!(expected, history.raw_items());
assert_eq!(initial_context, history.active_items());
}
#[tokio::test]
async fn clear_context_fails_when_turn_in_progress() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = sess.build_initial_context(tc.as_ref()).await;
sess.record_into_history(&initial_context, tc.as_ref())
.await;
*sess.active_turn.lock().await = Some(crate::state::ActiveTurn::default());
handlers::clear_context(&sess, "sub-1".to_string()).await;
let error_event = wait_for_context_clear_failed(&rx).await;
assert_eq!(
error_event.codex_error_info,
Some(CodexErrorInfo::ContextClearFailed)
);
let history = sess.clone_history().await;
assert_eq!(initial_context, history.raw_items());
}
#[tokio::test]
async fn set_rate_limits_retains_previous_credits() {
let codex_home = tempfile::tempdir().expect("create temp dir");
@@ -4206,6 +4308,44 @@ mod tests {
}
}
async fn wait_for_context_cleared(
rx: &async_channel::Receiver<Event>,
) -> crate::protocol::ContextClearedEvent {
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::ContextCleared(payload) => return payload,
_ => continue,
}
}
}
async fn wait_for_context_clear_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::ContextClearFailed) =>
{
return payload;
}
_ => continue,
}
}
}
fn text_block(s: &str) -> ContentBlock {
ContentBlock::TextContent(TextContent {
annotations: None,

View File

@@ -171,7 +171,7 @@ async fn run_compact_task_inner(
}
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let history_items = history_snapshot.active_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);

View File

@@ -44,7 +44,7 @@ async fn run_remote_compact_task_inner_impl(
// Required to keep `/undo` available after compaction
let ghost_snapshots: Vec<ResponseItem> = history
.raw_items()
.active_items()
.iter()
.filter(|item| matches!(item, ResponseItem::GhostSnapshot { .. }))
.cloned()

View File

@@ -71,6 +71,9 @@ impl ContextManager {
/// Returns the history prepared for sending to the model. This applies a proper
/// normalization and drop un-suited items.
pub(crate) fn for_prompt(mut self) -> Vec<ResponseItem> {
if let Some(clear_idx) = last_context_cleared_index(&self.items) {
self.items.drain(..=clear_idx);
}
self.normalize_history();
self.items
.retain(|item| !matches!(item, ResponseItem::GhostSnapshot { .. }));
@@ -82,6 +85,12 @@ impl ContextManager {
&self.items
}
/// Returns the effective items after the most recent context clear marker.
pub(crate) fn active_items(&self) -> &[ResponseItem] {
let start_idx = first_active_index(&self.items);
&self.items[start_idx..]
}
// Estimate token usage using byte-based heuristics from the truncation helpers.
// This is a coarse lower bound, not a tokenizer-accurate count.
pub(crate) fn estimate_token_count(&self, turn_context: &TurnContext) -> Option<i64> {
@@ -92,7 +101,7 @@ impl ContextManager {
let base_instructions = model_info.get_model_instructions(personality);
let base_tokens = i64::try_from(approx_token_count(&base_instructions)).unwrap_or(i64::MAX);
let items_tokens = self.items.iter().fold(0i64, |acc, item| {
let items_tokens = self.active_items().iter().fold(0i64, |acc, item| {
acc + match item {
ResponseItem::GhostSnapshot { .. } => 0,
ResponseItem::Reasoning {
@@ -117,10 +126,10 @@ impl ContextManager {
}
pub(crate) fn remove_first_item(&mut self) {
if !self.items.is_empty() {
// Remove the oldest item (front of the list). Items are ordered from
// oldest → newest, so index 0 is the first entry recorded.
let removed = self.items.remove(0);
let active_start = first_active_index(&self.items);
if active_start < self.items.len() {
// Remove the oldest *effective* item after the most recent clear marker.
let removed = self.items.remove(active_start);
// If the removed item participates in a call/output pair, also remove
// its corresponding counterpart to keep the invariants intact without
// running a full normalization pass.
@@ -135,12 +144,14 @@ impl ContextManager {
/// Replace image content in the last turn if it originated from a tool output.
/// Returns true when a tool image was replaced, false otherwise.
pub(crate) fn replace_last_turn_images(&mut self, placeholder: &str) -> bool {
let Some(index) = self.items.iter().rposition(|item| {
let start_idx = first_active_index(&self.items);
let Some(rel_index) = self.items[start_idx..].iter().rposition(|item| {
matches!(item, ResponseItem::FunctionCallOutput { .. })
|| matches!(item, ResponseItem::Message { role, .. } if role == "user")
}) else {
return false;
};
let index = start_idx + rel_index;
match &mut self.items[index] {
ResponseItem::FunctionCallOutput { output, .. } => {
@@ -179,7 +190,11 @@ impl ContextManager {
}
let snapshot = self.items.clone();
let user_positions = user_message_positions(&snapshot);
let active_start = first_active_index(&snapshot);
let prefix = snapshot[..active_start].to_vec();
let active = snapshot[active_start..].to_vec();
let user_positions = user_message_positions(&active);
let Some(&first_user_idx) = user_positions.first() else {
self.replace(snapshot);
return;
@@ -192,7 +207,9 @@ impl ContextManager {
user_positions[user_positions.len() - n_from_end]
};
self.replace(snapshot[..cut_idx].to_vec());
let mut new_items = prefix;
new_items.extend(active[..cut_idx].iter().cloned());
self.replace(new_items);
}
pub(crate) fn update_token_info(
@@ -208,17 +225,17 @@ impl ContextManager {
}
fn get_non_last_reasoning_items_tokens(&self) -> usize {
let active_start = first_active_index(&self.items);
let active_items = &self.items[active_start..];
// get reasoning items excluding all the ones after the last user message
let Some(last_user_index) = self
.items
let Some(last_user_index) = active_items
.iter()
.rposition(|item| matches!(item, ResponseItem::Message { role, .. } if role == "user"))
else {
return 0usize;
};
let total_reasoning_bytes = self
.items
let total_reasoning_bytes = active_items
.iter()
.take(last_user_index)
.filter_map(|item| {
@@ -301,13 +318,14 @@ impl ContextManager {
| ResponseItem::CustomToolCall { .. }
| ResponseItem::Compaction { .. }
| ResponseItem::GhostSnapshot { .. }
| ResponseItem::ContextCleared
| ResponseItem::Other => item.clone(),
}
}
}
/// API messages include every non-system item (user/assistant messages, reasoning,
/// tool calls, tool outputs, shell calls, and web-search calls).
/// tool calls, tool outputs, shell calls, web-search calls, and context markers).
fn is_api_message(message: &ResponseItem) -> bool {
match message {
ResponseItem::Message { role, .. } => role.as_str() != "system",
@@ -318,12 +336,23 @@ fn is_api_message(message: &ResponseItem) -> bool {
| ResponseItem::LocalShellCall { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::Compaction { .. } => true,
| ResponseItem::Compaction { .. }
| ResponseItem::ContextCleared => true,
ResponseItem::GhostSnapshot { .. } => false,
ResponseItem::Other => false,
}
}
fn last_context_cleared_index(items: &[ResponseItem]) -> Option<usize> {
items
.iter()
.rposition(|item| matches!(item, ResponseItem::ContextCleared))
}
fn first_active_index(items: &[ResponseItem]) -> usize {
last_context_cleared_index(items).map_or(0, |idx| idx.saturating_add(1))
}
fn estimate_reasoning_length(encoded_len: usize) -> usize {
encoded_len
.saturating_mul(3)

View File

@@ -28,7 +28,8 @@ pub(crate) fn should_persist_response_item(item: &ResponseItem) -> bool {
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::GhostSnapshot { .. }
| ResponseItem::Compaction { .. } => true,
| ResponseItem::Compaction { .. }
| ResponseItem::ContextCleared => true,
ResponseItem::Other => false,
}
}
@@ -43,6 +44,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool {
| EventMsg::AgentReasoningRawContent(_)
| EventMsg::TokenCount(_)
| EventMsg::ContextCompacted(_)
| EventMsg::ContextCleared(_)
| EventMsg::EnteredReviewMode(_)
| EventMsg::ExitedReviewMode(_)
| EventMsg::ThreadRolledBack(_)

View File

@@ -14,9 +14,10 @@ use codex_protocol::protocol::RolloutItem;
/// A user message boundary is a `RolloutItem::ResponseItem(ResponseItem::Message { .. })`
/// whose parsed turn item is `TurnItem::UserMessage`.
///
/// Rollouts can contain `ThreadRolledBack` markers. Those markers indicate that the
/// last N user turns were removed from the effective thread history; we apply them here so
/// indexing uses the post-rollback history rather than the raw stream.
/// Rollouts can contain context reset markers (e.g., `ThreadRolledBack`,
/// `ContextCleared`). Those markers indicate that older user turns no longer
/// contribute to the effective thread history; we apply them here so indexing
/// uses the post-reset history rather than the raw stream.
pub(crate) fn user_message_positions_in_rollout(items: &[RolloutItem]) -> Vec<usize> {
let mut user_positions = Vec::new();
for (idx, item) in items.iter().enumerate() {
@@ -34,6 +35,9 @@ pub(crate) fn user_message_positions_in_rollout(items: &[RolloutItem]) -> Vec<us
let new_len = user_positions.len().saturating_sub(num_turns);
user_positions.truncate(new_len);
}
RolloutItem::EventMsg(EventMsg::ContextCleared(_)) => {
user_positions.clear();
}
_ => {}
}
}
@@ -75,6 +79,7 @@ mod tests {
use assert_matches::assert_matches;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::protocol::ContextClearedEvent;
use codex_protocol::protocol::ThreadRolledBackEvent;
use pretty_assertions::assert_eq;
@@ -188,6 +193,26 @@ mod tests {
);
}
#[test]
fn truncates_rollout_from_start_respects_context_cleared_marker() {
let rollout_items = vec![
RolloutItem::ResponseItem(user_msg("u1")),
RolloutItem::ResponseItem(assistant_msg("a1")),
RolloutItem::EventMsg(EventMsg::ContextCleared(ContextClearedEvent)),
RolloutItem::ResponseItem(user_msg("u2")),
RolloutItem::ResponseItem(assistant_msg("a2")),
RolloutItem::ResponseItem(user_msg("u3")),
RolloutItem::ResponseItem(assistant_msg("a3")),
];
let truncated = truncate_rollout_before_nth_user_message_from_start(&rollout_items, 1);
let expected = rollout_items[..5].to_vec();
assert_eq!(
serde_json::to_value(&truncated).unwrap(),
serde_json::to_value(&expected).unwrap()
);
}
#[tokio::test]
async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
let (session, turn_context) = make_session_and_context().await;

View File

@@ -66,19 +66,20 @@ impl SessionTask for UndoTask {
let history = sess.clone_history().await;
let mut items = history.raw_items().to_vec();
let active_start = items.len().saturating_sub(history.active_items().len());
let mut completed = UndoCompletedEvent {
success: false,
message: None,
};
let Some((idx, ghost_commit)) =
items
items[active_start..]
.iter()
.enumerate()
.rev()
.find_map(|(idx, item)| match item {
.find_map(|(rel_idx, item)| match item {
ResponseItem::GhostSnapshot { ghost_commit } => {
Some((idx, ghost_commit.clone()))
Some((active_start + rel_idx, ghost_commit.clone()))
}
_ => None,
})

View File

@@ -68,6 +68,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
- `Op::UserTurn` Any input from the user to kick off a `Turn`
- `Op::UserInput` Legacy form of user input
- `Op::Interrupt` Interrupts a running turn
- `Op::ClearContext` Reset the conversation back to the initial context boundary
- `Op::ExecApproval` Approve or deny code execution
- `Op::UserInputAnswer` Provide answers for a `request_user_input` tool call
- `Op::ListSkills` Request skills for one or more cwd values (optionally `force_reload`)
@@ -76,6 +77,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
- `EventMsg::AgentMessage` Messages from the `Model`
- `EventMsg::ExecApprovalRequest` Request approval from user to execute a command
- `EventMsg::RequestUserInput` Request user input for a tool call
- `EventMsg::ContextCleared` Signals that prior conversation history should be cleared
- `EventMsg::TurnComplete` A turn completed successfully
- `EventMsg::Error` A turn stopped with an error
- `EventMsg::Warning` A non-fatal warning that the client should surface to the user

View File

@@ -607,6 +607,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
| EventMsg::UndoCompleted(_)
| EventMsg::UndoStarted(_)
| EventMsg::ThreadRolledBack(_)
| EventMsg::ContextCleared(_)
| EventMsg::RequestUserInput(_) => {}
}
CodexStatus::Running

View File

@@ -361,6 +361,7 @@ async fn run_codex_tool_session_inner(
| EventMsg::ExitedReviewMode(_)
| EventMsg::RequestUserInput(_)
| EventMsg::ContextCompacted(_)
| EventMsg::ContextCleared(_)
| EventMsg::ThreadRolledBack(_)
| EventMsg::CollabAgentSpawnBegin(_)
| EventMsg::CollabAgentSpawnEnd(_)

View File

@@ -503,6 +503,7 @@ impl OtelManager {
ResponseItem::WebSearchCall { .. } => "web_search_call".into(),
ResponseItem::GhostSnapshot { .. } => "ghost_snapshot".into(),
ResponseItem::Compaction { .. } => "compaction".into(),
ResponseItem::ContextCleared => "context_cleared".into(),
ResponseItem::Other => "other".into(),
}
}

View File

@@ -163,6 +163,7 @@ pub enum ResponseItem {
GhostSnapshot {
ghost_commit: GhostCommit,
},
ContextCleared,
#[serde(alias = "compaction_summary")]
Compaction {
encrypted_content: String,

View File

@@ -265,6 +265,13 @@ pub enum Op {
/// responsible for undoing any edits on disk.
ThreadRollback { num_turns: u32 },
/// Request Codex to clear the effective context while keeping initial context.
///
/// This is append-only: a context-cleared marker is recorded alongside a
/// fresh copy of the initial context, and prompt construction ignores the
/// earlier conversation history.
ClearContext,
/// Request a code review from the agent.
Review { review_request: ReviewRequest },
@@ -675,6 +682,9 @@ pub enum EventMsg {
/// Conversation history was compacted (either automatically or manually).
ContextCompacted(ContextCompactedEvent),
/// Conversation context was cleared back to the initial context.
ContextCleared(ContextClearedEvent),
/// Conversation history was rolled back by dropping the last N user turns.
ThreadRolledBack(ThreadRolledBackEvent),
@@ -927,6 +937,7 @@ pub enum CodexErrorInfo {
http_status_code: Option<u16>,
},
ThreadRollbackFailed,
ContextClearFailed,
Other,
}
@@ -1066,6 +1077,9 @@ pub struct WarningEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ContextCompactedEvent;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ContextClearedEvent;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnCompleteEvent {
pub last_agent_message: Option<String>,

View File

@@ -821,13 +821,17 @@ impl App {
Ok(())
}
fn reset_for_thread_switch(&mut self, tui: &mut tui::Tui) -> Result<()> {
fn reset_transcript_state(&mut self) {
self.overlay = None;
self.transcript_cells.clear();
self.deferred_history_lines.clear();
self.has_emitted_history_lines = false;
self.backtrack = BacktrackState::default();
self.backtrack_render_pending = false;
}
fn reset_for_thread_switch(&mut self, tui: &mut tui::Tui) -> Result<()> {
self.reset_transcript_state();
tui.terminal.clear_scrollback()?;
tui.terminal.clear()?;
Ok(())
@@ -2050,12 +2054,31 @@ impl App {
self.chat_widget.handle_codex_event(event);
}
fn reset_for_context_clear(&mut self) {
self.reset_transcript_state();
if let Some(thread_id) = self
.active_thread_id
.or_else(|| self.chat_widget.thread_id())
{
let session_configured = self.session_configured_for_thread(thread_id);
self.chat_widget.reset_for_context_clear(session_configured);
}
}
fn handle_codex_event_replay(&mut self, event: Event) {
if matches!(&event.msg, EventMsg::ContextCleared(_)) {
self.reset_for_context_clear();
}
self.handle_backtrack_event(&event.msg);
self.chat_widget.handle_codex_event_replay(event);
}
fn handle_active_thread_event(&mut self, tui: &mut tui::Tui, event: Event) -> Result<()> {
if matches!(&event.msg, EventMsg::ContextCleared(_)) {
self.reset_for_context_clear();
tui.terminal.clear_scrollback()?;
tui.terminal.clear()?;
}
self.handle_codex_event_now(event);
if self.backtrack_render_pending {
tui.frame_requester().schedule_frame();

View File

@@ -454,6 +454,7 @@ impl App {
pub(crate) fn handle_backtrack_event(&mut self, event: &EventMsg) {
match event {
EventMsg::ThreadRolledBack(_) => self.finish_pending_backtrack(),
EventMsg::ContextCleared(_) => self.backtrack.pending_rollback = None,
EventMsg::Error(ErrorEvent {
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
..

View File

@@ -763,6 +763,57 @@ impl ChatWidget {
}
}
pub(crate) fn reset_for_context_clear(
&mut self,
event: codex_core::protocol::SessionConfiguredEvent,
) {
self.active_cell = None;
self.active_cell_revision = self.active_cell_revision.wrapping_add(1);
self.stream_controller = None;
self.running_commands.clear();
self.suppressed_exec_calls.clear();
self.last_unified_wait = None;
self.unified_exec_wait_streak = None;
self.task_complete_pending = false;
self.clear_unified_exec_processes();
self.agent_turn_running = false;
self.update_task_running_state();
self.interrupts = InterruptManager::default();
self.reasoning_buffer.clear();
self.full_reasoning_buffer.clear();
self.retry_status_header = None;
self.queued_user_messages.clear();
self.pending_notification = None;
self.needs_final_message_separator = false;
self.had_work_activity = false;
self.saw_plan_update_this_turn = false;
self.last_separator_elapsed_secs = None;
self.last_rendered_width.set(None);
self.clear_token_usage();
self.refresh_queued_user_messages();
self.bottom_pane
.set_history_metadata(event.history_log_id, event.history_entry_count);
self.thread_id = Some(event.session_id);
self.forked_from = event.forked_from_id;
self.current_rollout_path = event.rollout_path.clone();
let model_for_header = event.model.clone();
let reasoning_effort = event
.reasoning_effort
.or_else(|| self.current_collaboration_mode.reasoning_effort());
self.session_header.set_model(&model_for_header);
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
Some(model_for_header.clone()),
Some(reasoning_effort),
None,
);
self.refresh_model_display();
let session_info_cell =
history_cell::new_session_info(&self.config, &model_for_header, event, false);
self.apply_session_info_cell(session_info_cell);
self.request_redraw();
}
fn set_skills(&mut self, skills: Option<Vec<SkillMetadata>>) {
self.bottom_pane.set_skills(skills);
}
@@ -3025,7 +3076,7 @@ impl ChatWidget {
EventMsg::CollabWaitingEnd(ev) => self.on_collab_event(collab::waiting_end(ev)),
EventMsg::CollabCloseBegin(_) => {}
EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)),
EventMsg::ThreadRolledBack(_) => {}
EventMsg::ThreadRolledBack(_) | EventMsg::ContextCleared(_) => {}
EventMsg::RawResponseItem(_)
| EventMsg::ItemStarted(_)
| EventMsg::ItemCompleted(_)