mirror of
https://github.com/openai/codex.git
synced 2026-02-03 15:33:41 +00:00
Compare commits
2 Commits
dev/cc/new
...
clear-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
710eb7cf6f | ||
|
|
1aea5ff6c1 |
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -607,6 +607,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
| EventMsg::UndoCompleted(_)
|
||||
| EventMsg::UndoStarted(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::ContextCleared(_)
|
||||
| EventMsg::RequestUserInput(_) => {}
|
||||
}
|
||||
CodexStatus::Running
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ pub enum ResponseItem {
|
||||
GhostSnapshot {
|
||||
ghost_commit: GhostCommit,
|
||||
},
|
||||
ContextCleared,
|
||||
#[serde(alias = "compaction_summary")]
|
||||
Compaction {
|
||||
encrypted_content: String,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
..
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
Reference in New Issue
Block a user