Replay thread rollback from rollout history (#13615)

- Replay thread rollback from the persisted rollout history instead of
truncating in-memory state.\n- Add rollback coverage, including
rollback-behind-compaction snapshot coverage.
This commit is contained in:
Ahmed Ibrahim
2026-03-05 16:40:09 -08:00
committed by GitHub
parent 6cf0ed4e79
commit 629cb15bc6
3 changed files with 525 additions and 76 deletions

View File

@@ -20,10 +20,15 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
use core_test_support::context_snapshot::ContextSnapshotRenderMode;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::ResponsesRequest;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::mount_sse_once_match;
use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
@@ -35,6 +40,7 @@ use tempfile::TempDir;
use wiremock::MockServer;
const AFTER_SECOND_RESUME: &str = "AFTER_SECOND_RESUME";
const AFTER_ROLLBACK: &str = "AFTER_ROLLBACK";
fn network_disabled() -> bool {
std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok()
@@ -406,6 +412,95 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
/// Scenario: rolling back behind a pre-turn compaction should replay
/// append-only history from the rollout file and keep earlier compacted
/// history visible.
async fn snapshot_rollback_past_compaction_replays_append_only_history() -> Result<()> {
if network_disabled() {
println!("Skipping test because network is disabled in this sandbox");
return Ok(());
}
const EDITED_AFTER_COMPACT: &str = "EDITED_AFTER_COMPACT";
const SECOND_REPLY: &str = "SECOND_REPLY";
let server = MockServer::start().await;
let sse1 = sse(vec![
ev_assistant_message("m1", FIRST_REPLY),
ev_completed("r1"),
]);
let sse2 = sse(vec![
ev_assistant_message("m2", SUMMARY_TEXT),
ev_completed("r2"),
]);
let sse3 = sse(vec![
ev_assistant_message("m3", SECOND_REPLY),
ev_completed("r3"),
]);
let sse4 = sse(vec![ev_completed("r4")]);
let request_log = mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await;
let (_home, _config, _manager, base) = start_test_conversation(&server, None).await;
user_turn(&base, "hello world").await;
compact_conversation(&base).await;
user_turn(&base, EDITED_AFTER_COMPACT).await;
base.submit(Op::ThreadRollback { num_turns: 1 })
.await
.expect("submit thread rollback");
let rollback_event =
wait_for_event(&base, |ev| matches!(ev, EventMsg::ThreadRolledBack(_))).await;
let EventMsg::ThreadRolledBack(rollback_event) = rollback_event else {
panic!("expected thread rolled back event");
};
assert_eq!(rollback_event.num_turns, 1);
user_turn(&base, AFTER_ROLLBACK).await;
let requests = request_log.requests();
assert_eq!(requests.len(), 4);
assert!(requests[1].body_contains_text(SUMMARIZATION_PROMPT));
assert!(requests[2].body_contains_text("hello world"));
assert!(requests[2].body_contains_text(SUMMARY_TEXT));
assert!(requests[2].body_contains_text(EDITED_AFTER_COMPACT));
let after_rollback_user_texts = requests[3].message_input_texts("user");
let after_rollback_last = after_rollback_user_texts
.last()
.unwrap_or_else(|| panic!("post-rollback request missing user messages"));
assert_eq!(after_rollback_last, AFTER_ROLLBACK);
assert!(
requests[3].body_contains_text("hello world"),
"the first turn should remain visible after rollback behind compaction",
);
assert!(
!requests[3].body_contains_text(EDITED_AFTER_COMPACT),
"the edited post-compaction turn should be removed by rollback",
);
assert!(
requests[3].body_contains_text(SUMMARY_TEXT),
"compaction summary should remain for the preserved first turn",
);
insta::assert_snapshot!(
"rollback_past_compaction_shapes",
context_snapshot::format_labeled_requests_snapshot(
"rollback past compaction replay after rollback",
&[
("compaction request", &requests[1]),
("before rollback", &requests[2]),
("after rollback", &requests[3]),
],
&ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
)
);
Ok(())
}
fn normalize_line_endings(value: &mut Value) {
match value {
Value::String(text) => {
@@ -427,10 +522,16 @@ fn normalize_line_endings(value: &mut Value) {
}
}
fn gather_request_bodies(request_log: &[ResponseMock]) -> Vec<Value> {
let mut bodies = request_log
fn gather_requests(request_log: &[ResponseMock]) -> Vec<ResponsesRequest> {
request_log
.iter()
.flat_map(ResponseMock::requests)
.collect::<Vec<_>>()
}
fn gather_request_bodies(request_log: &[ResponseMock]) -> Vec<Value> {
let mut bodies = gather_requests(request_log)
.into_iter()
.map(|request| request.body_json())
.collect::<Vec<_>>();
bodies.iter_mut().for_each(normalize_line_endings);

View File

@@ -0,0 +1,36 @@
---
source: core/tests/suite/compact_resume_fork.rs
expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback past compaction replay after rollback\",\n&[(\"compaction request\", &requests[1]), (\"before rollback\", &requests[2]),\n(\"after rollback\", &requests[3]),],\n&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 64 }),)"
---
Scenario: rollback past compaction replay after rollback
## compaction request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:hello world
03:message/assistant:FIRST_REPLY
04:message/user:<SUMMARIZATION_PROMPT>
## before rollback
00:message/user:hello world
01:message/user:<COMPACTION_SUMMARY>\nSUMMARY_ONLY_CONTEXT
02:message/developer:<PERMISSIONS_INSTRUCTIONS>
03:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
04:message/user:EDITED_AFTER_COMPACT
## after rollback
00:message/user:hello world
01:message/user:<COMPACTION_SUMMARY>\nSUMMARY_ONLY_CONTEXT
02:message/developer:<PERMISSIONS_INSTRUCTIONS>
03:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
04:message/developer:<PERMISSIONS_INSTRUCTIONS>
05:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
06:message/user:AFTER_ROLLBACK