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
+6 -2
View File
@@ -2156,6 +2156,11 @@ pub struct TurnCompleteEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnStartedEvent {
/// Provenance of a regular turn, persisted before startup work can be suspended.
/// Absent on older records and non-regular tasks.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub turn_attribution: Option<crate::turn_input::TurnAttribution>,
pub turn_id: String,
/// ID of the originating turn in the root thread; equals `turn_id` for root turns.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -3280,8 +3285,7 @@ pub struct TurnContextNetworkItem {
pub struct TurnContextItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
/// Root turn that owns this subagent turn's attribution.
/// Only set for subagent turns; persisted so resume keeps the scope frozen at turn start.
/// Root turn that owns this turn's attribution, retained across recovery.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_turn_id: Option<String>,
/// Plugin selection captured for this turn. Absent in older histories.