mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
Preserve thread context when retrying or editing turns (#33211)
## What changed - Add experimental `beforeTurnId` support to `thread/fork` for copying history strictly before a turn, including an in-progress turn. - Use before-turn forks for TUI backtracking and safety retries so the replacement prompt retains thread context instead of recreating the branch from the preceding turn or a new thread. - Add experimental `deferGoalContinuation` support so safety-retry forks can inherit an active goal while waiting for the replacement turn before automatic continuation resumes. - Hide duplicated nested-review prompts when replaying history or selecting a prompt to edit. ## Testing - Cover before-turn truncation, fork validation, active-goal inheritance, TUI safety retries, backtracking, and nested-review replay. GitOrigin-RevId: 4915d85ae1929a0b8c40f3ed7dca29c7a5bdcdf3
This commit is contained in:
1 parent
e4711f2a3b
commit
d88db19144
25 files changed
+1343
-96
No files matched your search
@@ -12,7 +12,12 @@ impl ChatWidget {
|
||||
/// avoid triggering side effects. Event ids are passed as `None` to
|
||||
/// distinguish replayed events from live ones.
|
||||
pub(crate) fn replay_thread_turns(&mut self, turns: Vec<Turn>, replay_kind: ReplayKind) {
|
||||
for turn in turns {
|
||||
let hidden_nested_review_turns = std::iter::once(/*value*/ false)
|
||||
.chain(turns.windows(/*size*/ 2).map(|turns| {
|
||||
crate::app_backtrack::is_hidden_nested_review_turn(&turns[0], &turns[1])
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
for (turn, hidden_nested_review_turn) in turns.into_iter().zip(hidden_nested_review_turns) {
|
||||
let Turn {
|
||||
id: turn_id,
|
||||
items_view: _,
|
||||
@@ -29,8 +34,16 @@ impl ChatWidget {
|
||||
self.on_task_started();
|
||||
}
|
||||
for item in items {
|
||||
if hidden_nested_review_turn && matches!(item, ThreadItem::UserMessage { .. }) {
|
||||
continue;
|
||||
}
|
||||
self.replay_thread_item(item, turn_id.clone(), replay_kind);
|
||||
}
|
||||
let status = if hidden_nested_review_turn {
|
||||
TurnStatus::Completed
|
||||
} else {
|
||||
status
|
||||
};
|
||||
if matches!(
|
||||
status,
|
||||
TurnStatus::Completed | TurnStatus::Interrupted | TurnStatus::Failed
|
||||
|
||||
@@ -70,9 +70,7 @@ impl ChatWidget {
|
||||
self.input_queue.user_turn_pending_start = true;
|
||||
}
|
||||
|
||||
pub(crate) fn commit_safety_buffered_retry_submission(&mut self, prompt: UserMessage) {
|
||||
let display =
|
||||
user_message_display_for_history(prompt, &UserMessageHistoryRecord::UserMessageText);
|
||||
pub(crate) fn commit_safety_buffered_retry_submission(&mut self, display: UserMessageDisplay) {
|
||||
self.on_user_message_display(display);
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,89 @@ async fn replayed_review_prompt_does_not_seed_composer_history() {
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let review_hint = "current changes";
|
||||
let review_prompt = "Review the current code changes (staged, unstaged, and untracked files).";
|
||||
let user_message = |id: &str, text: &str| AppServerThreadItem::UserMessage {
|
||||
id: id.to_string(),
|
||||
client_id: None,
|
||||
content: vec![AppServerUserInput::Text {
|
||||
text: text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
};
|
||||
let review_marker = |turn_id: &str| AppServerTurn {
|
||||
items: vec![
|
||||
AppServerThreadItem::EnteredReviewMode {
|
||||
id: format!("{turn_id}-start"),
|
||||
review: review_hint.to_string(),
|
||||
},
|
||||
AppServerThreadItem::ExitedReviewMode {
|
||||
id: format!("{turn_id}-end"),
|
||||
review: "review complete".to_string(),
|
||||
},
|
||||
],
|
||||
..app_server_turn(
|
||||
turn_id,
|
||||
AppServerTurnStatus::Completed,
|
||||
/*duration_ms*/ None,
|
||||
/*error*/ None,
|
||||
)
|
||||
};
|
||||
|
||||
chat.replay_thread_turns(
|
||||
vec![
|
||||
review_marker("turn-review-before-steer"),
|
||||
AppServerTurn {
|
||||
items: vec![
|
||||
user_message("interrupted-prompt", review_hint),
|
||||
user_message("interrupted-steer", review_hint),
|
||||
],
|
||||
completed_at: Some(1),
|
||||
..app_server_turn(
|
||||
"turn-interrupted",
|
||||
AppServerTurnStatus::Interrupted,
|
||||
/*duration_ms*/ None,
|
||||
/*error*/ None,
|
||||
)
|
||||
},
|
||||
review_marker("turn-review"),
|
||||
AppServerTurn {
|
||||
items: vec![
|
||||
user_message("review-prompt-1", review_prompt),
|
||||
user_message("review-prompt-2", review_prompt),
|
||||
AppServerThreadItem::AgentMessage {
|
||||
id: "review-result".to_string(),
|
||||
text: "review result is retained".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
},
|
||||
],
|
||||
..app_server_turn(
|
||||
"turn-review-child",
|
||||
AppServerTurnStatus::Interrupted,
|
||||
/*duration_ms*/ None,
|
||||
/*error*/ None,
|
||||
)
|
||||
},
|
||||
],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let rendered = drain_insert_history(&mut rx)
|
||||
.into_iter()
|
||||
.map(|lines| lines_to_single_string(&lines))
|
||||
.collect::<String>();
|
||||
assert!(!rendered.contains(review_prompt));
|
||||
assert_eq!(rendered.matches(review_hint).count(), 4);
|
||||
insta::assert_snapshot!("replayed_nested_review_prompts", rendered);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
assert_eq!(chat.bottom_pane.composer_text(), review_hint);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/history_replay.rs
|
||||
expression: rendered
|
||||
---
|
||||
>> Code review started: current changes <<
|
||||
|
||||
<< Code review finished >>
|
||||
|
||||
|
||||
› current changes
|
||||
|
||||
|
||||
|
||||
› current changes
|
||||
|
||||
|
||||
■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.
|
||||
|
||||
>> Code review started: current changes <<
|
||||
|
||||
<< Code review finished >>
|
||||
|
||||
• review result is retained
|
||||
Reference in new issue
Block a user