[codex-core] Preserve ghost snapshots in compaction fallback [ci changed_files]

Keep same-turn ghost snapshots when pre-turn inline compaction downgrades to the legacy client-side path so undo state survives compatibility fallback.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Cooper Gamble
2026-03-11 02:48:35 +00:00
co-authored by Codex
parent ea2a4be3d8
commit dea81dba3a
2 changed files with 49 additions and 1 deletions
+24 -1
View File
@@ -5990,6 +5990,20 @@ struct PreTurnInlineCompactionState {
turn_context_item: TurnContextItem,
}
fn collect_new_ghost_snapshots_since(
history_before_turn: &[ResponseItem],
current_history: &[ResponseItem],
) -> Vec<ResponseItem> {
current_history
.iter()
.filter(|item| {
matches!(item, ResponseItem::GhostSnapshot { .. })
&& !history_before_turn.contains(item)
})
.cloned()
.collect()
}
fn record_compaction_metric(
sess: &Session,
mode: &'static str,
@@ -6127,8 +6141,17 @@ async fn downgrade_known_inline_compaction_error(
let Some(preturn_state) = preturn_state else {
return Ok(false);
};
// Preserve same-turn ghost snapshots that may have completed after
// the pre-turn baseline was captured so `/undo` still works after
// we downgrade to the legacy compaction path.
let current_history = sess.clone_history().await;
let mut restored_history = preturn_state.history_before_turn.clone();
restored_history.extend(collect_new_ghost_snapshots_since(
&preturn_state.history_before_turn,
current_history.raw_items(),
));
sess.replace_history(
preturn_state.history_before_turn.clone(),
restored_history,
preturn_state.reference_context_before_turn.clone(),
)
.await;
+25
View File
@@ -55,6 +55,7 @@ use crate::tools::registry::ToolHandler;
use crate::tools::router::ToolCallSource;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_app_server_protocol::AppInfo;
use codex_git::GhostCommit;
use codex_otel::TelemetryAuthMode;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
@@ -136,6 +137,12 @@ fn skill_message(text: &str) -> ResponseItem {
}
}
fn ghost_snapshot(id: &str) -> ResponseItem {
ResponseItem::GhostSnapshot {
ghost_commit: GhostCommit::new(id.to_string(), None, Vec::new(), Vec::new()),
}
}
fn developer_input_texts(items: &[ResponseItem]) -> Vec<&str> {
items
.iter()
@@ -253,6 +260,24 @@ fn assistant_message_stream_parsers_seed_plan_parser_across_added_and_delta_boun
assert!(tail.plan_segments.is_empty());
}
#[test]
fn collect_new_ghost_snapshots_since_returns_only_snapshots_added_after_turn_start() {
let prior_snapshot = ghost_snapshot("ghost-before");
let same_turn_snapshot = ghost_snapshot("ghost-during");
let history_before_turn = vec![user_message("earlier"), prior_snapshot.clone()];
let current_history = vec![
user_message("earlier"),
prior_snapshot,
user_message("current turn"),
assistant_message("in progress"),
same_turn_snapshot.clone(),
];
let new_snapshots = collect_new_ghost_snapshots_since(&history_before_turn, &current_history);
assert_eq!(new_snapshots, vec![same_turn_snapshot]);
}
fn make_mcp_tool(
server_name: &str,
tool_name: &str,