mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Preserve turn attribution across recovery and compaction (#51402)
## Why Recovering an interrupted turn replaced its original trigger with `retry` and restored only its root lineage from model context. Turns suspended before that context was written could lose their attribution entirely. ## What changed - Persist regular-turn attribution in `TurnStartedEvent` and compaction resume metadata, including the trigger, parent and root turn IDs, and initiating agent path. - Restore matching attribution during recovery, keep unknown triggers absent, and retain the model-context root fallback for older records. - Reconstruct attribution from surviving turns across rollback and late completion or abort events, and clear inherited attribution when spawning a forked agent. ## Testing Add coverage for repeated cold recovery, suspension before context persistence, legacy rollouts, fresh attribution on subsequent turns, compaction checkpoints, rollback, and late terminal events. GitOrigin-RevId: 85b5763e8fbd48a1ea2c07cfda8196e63c2cd33a
This commit is contained in:
BIN
Binary file not shown.
@@ -108,6 +108,7 @@ fn guardian_stdin_reviews_preserve_parent_command_history() {
|
||||
};
|
||||
let start_turn = |turn_id: &str| {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
|
||||
@@ -2186,6 +2186,7 @@ mod tests {
|
||||
let thread_id = ThreadId::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -2252,6 +2253,7 @@ mod tests {
|
||||
}));
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -2321,6 +2323,7 @@ mod tests {
|
||||
let saved_path = test_path_buf("/tmp/image-1.png").abs();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -2414,6 +2417,7 @@ mod tests {
|
||||
});
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -2533,6 +2537,7 @@ mod tests {
|
||||
let thread_id = ThreadId::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -2634,6 +2639,7 @@ mod tests {
|
||||
fn replays_image_generation_end_events_into_turn_history() {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-image".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3029,6 +3035,7 @@ mod tests {
|
||||
fn uses_explicit_turn_boundaries_for_mid_turn_steering() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3097,6 +3104,7 @@ mod tests {
|
||||
fn reconstructs_tool_items_from_persisted_completion_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3235,6 +3243,7 @@ mod tests {
|
||||
fn reconstructs_mcp_tool_result_meta_from_persisted_completion_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3318,6 +3327,7 @@ mod tests {
|
||||
fn reconstructs_dynamic_tool_items_from_request_and_response_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3403,6 +3413,7 @@ mod tests {
|
||||
fn reconstructs_declined_exec_and_patch_items() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3499,6 +3510,7 @@ mod tests {
|
||||
fn reconstructs_declined_guardian_command_item() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3598,6 +3610,7 @@ mod tests {
|
||||
fn reconstructs_in_progress_guardian_execve_item() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3706,6 +3719,7 @@ mod tests {
|
||||
};
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3727,6 +3741,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3767,6 +3782,7 @@ mod tests {
|
||||
fn assigns_late_exec_completion_to_original_turn() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3792,6 +3808,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3874,6 +3891,7 @@ mod tests {
|
||||
fn drops_late_turn_scoped_item_for_unknown_turn_id() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3899,6 +3917,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -3975,6 +3994,7 @@ mod tests {
|
||||
None
|
||||
);
|
||||
builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4044,6 +4064,7 @@ mod tests {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
assert_eq!(builder.active_turn_metadata_snapshot(), None);
|
||||
builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".into(),
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
trace_id: None,
|
||||
@@ -4092,6 +4113,7 @@ mod tests {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4161,6 +4183,7 @@ mod tests {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4230,6 +4253,7 @@ mod tests {
|
||||
fn late_turn_complete_does_not_close_active_turn() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4255,6 +4279,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4312,6 +4337,7 @@ mod tests {
|
||||
fn late_turn_complete_with_embedded_error_preserves_active_turn() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4328,6 +4354,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4416,6 +4443,7 @@ mod tests {
|
||||
fn late_turn_aborted_does_not_interrupt_active_turn() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4441,6 +4469,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4489,6 +4518,7 @@ mod tests {
|
||||
fn preserves_compaction_only_turn() {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-compact".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4763,6 +4793,7 @@ mod tests {
|
||||
fn out_of_turn_error_does_not_create_or_fail_a_turn() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4826,6 +4857,7 @@ mod tests {
|
||||
fn error_then_turn_complete_preserves_failed_status() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4886,6 +4918,7 @@ mod tests {
|
||||
fn turn_complete_with_embedded_error_marks_turn_failed() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -4957,6 +4990,7 @@ mod tests {
|
||||
.expect("hook prompt message");
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -5018,6 +5052,7 @@ mod tests {
|
||||
let expected_item = ThreadItem::from(hook_prompt.clone());
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -5047,6 +5082,7 @@ mod tests {
|
||||
.expect("worker path");
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -5095,6 +5131,7 @@ mod tests {
|
||||
fn ignores_plain_user_response_items_in_rollout_replay() {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
@@ -5256,6 +5293,7 @@ mod tests {
|
||||
|
||||
let start_changes = builder.handle_rollout_item_with_changes(&RolloutItem::EventMsg(
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
trace_id: None,
|
||||
@@ -5374,6 +5412,7 @@ mod tests {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
let changes = builder.handle_rollout_items_with_changes(&[
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
trace_id: None,
|
||||
@@ -5415,6 +5454,7 @@ mod tests {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
let changes = builder.handle_rollout_items_with_changes(&[
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
trace_id: None,
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::protocol::v2::TurnError;
|
||||
fn projects_turn_lifecycle_without_prior_builder_state() {
|
||||
let started = project(RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
trace_id: None,
|
||||
|
||||
Reference in New Issue
Block a user