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:
Eddie Chen
2026-10-06 15:50:27 +00:00
committed by copyberry
parent a4ebc509f4
commit 551bd409eb
50 changed files with 912 additions and 38 deletions
+1
View File
@@ -1201,6 +1201,7 @@ impl LocalAgentControl {
compacted.latest_token_usage_record = None;
if let Some(resume_metadata) = &mut compacted.resume_metadata {
resume_metadata.multi_agent_version = Some(multi_agent_version);
resume_metadata.turn_attribution = None;
if !preserve_context_baselines {
resume_metadata.previous_turn_settings = None;
}
+12 -1
View File
@@ -565,6 +565,7 @@ async fn get_status_returns_not_found_without_manager() {
#[tokio::test]
async fn on_event_updates_status_from_task_started() {
let status = agent_status_from_event(&EventMsg::TurnStarted(TurnStartedEvent {
turn_attribution: None,
turn_id: "turn-1".to_string(),
root_turn_id: None,
trace_id: None,
@@ -2256,7 +2257,14 @@ async fn spawn_agent_fork_sanitizes_inherited_compaction_metadata() {
let (parent_thread_id, parent_thread) = harness.start_paginated_thread().await;
let parent_resume_metadata = codex_history::CompactionResumeMetadata {
multi_agent_version: Some(MultiAgentVersion::V2),
last_started_turn_id: Some("parent-turn".into()),
last_started_turn_id: Some("parent-turn".to_string()),
turn_attribution: Some(codex_history::TurnAttribution {
turn_id: "parent-turn".to_string(),
turn_trigger: Some("automation".to_string()),
parent_turn_id: Some("initiating-turn".to_string()),
initiating_agent_path: Some(codex_protocol::AgentPath::root()),
root_turn_id: Some("root-turn".to_string()),
}),
previous_turn_settings: Some(codex_history::PreviousTurnSettings {
model: "parent-model".into(),
comp_hash: None,
@@ -2382,6 +2390,7 @@ async fn spawn_agent_fork_sanitizes_inherited_compaction_metadata() {
inherited_resume_metadata,
&codex_history::CompactionResumeMetadata {
multi_agent_version: Some(MultiAgentVersion::V1),
turn_attribution: None,
..parent_resume_metadata
}
);
@@ -3287,6 +3296,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
resume_metadata: Some(codex_history::CompactionResumeMetadata {
multi_agent_version: Some(MultiAgentVersion::V2),
last_started_turn_id: None,
turn_attribution: None,
previous_turn_settings: Some(codex_history::PreviousTurnSettings {
model: "parent-model".into(),
comp_hash: None,
@@ -3380,6 +3390,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
Some(codex_history::CompactionResumeMetadata {
multi_agent_version: Some(MultiAgentVersion::V2),
last_started_turn_id: None,
turn_attribution: None,
previous_turn_settings: None,
})
);