Persist resume metadata in compaction checkpoints (#47323)

## Why

Compaction companion records do not represent all resume state, including the last started turn and previous turn settings. The selected multi-agent runtime also needs a persisted value when no companion turn context is present.

## What changed

- Add optional `resume_metadata` to `CompactedItem`, recording the multi-agent version, last started turn ID, and previous turn settings. Preserve the distinction between absent metadata and explicitly empty values while retaining compatibility with older records.
- Capture this metadata under the settings persistence lock alongside the compaction checkpoint.
- Rewrite inherited metadata to use the child agent's runtime, and clear previous turn settings when the child must rebuild context.
- Clear pending continuation state before delivering acceptance of standalone settings updates, while holding the persistence lock.

## Testing

Add coverage for metadata round trips with empty values, legacy reader compatibility, checkpoint companion records, inherited fork metadata, and continuation invalidation while settings event delivery is blocked.

GitOrigin-RevId: f30b7feaa7e2fdb5b65fc3c53b37fd7a7fe27903
This commit is contained in:
Owen Lin
2026-09-22 16:35:30 +00:00
committed by copyberry
parent 5a11c45606
commit 286d4ecf44
23 changed files with 340 additions and 63 deletions
+18 -12
View File
@@ -70,7 +70,7 @@ pub(super) fn agent_nickname_candidates(config: &Config, role_name: Option<&str>
.collect()
}
fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item: bool) -> bool {
fn keep_forked_rollout_item(item: &RolloutItem, preserve_context_baselines: bool) -> bool {
match item {
RolloutItem::ResponseItem(envelope) => match &envelope.item {
ResponseItem::Message { role, phase, .. } => match role.as_str() {
@@ -107,7 +107,7 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
// Full-history forks preserve the cached prompt prefix and can keep diffing
// from the parent's durable baseline. Truncated forks drop part of that prompt,
// so they must rebuild context on their first child turn.
RolloutItem::TurnContext(_) | RolloutItem::WorldState(_) => preserve_reference_context_item,
RolloutItem::TurnContext(_) | RolloutItem::WorldState(_) => preserve_context_baselines,
// Child threads inherit model context, not the parent's cumulative usage state.
RolloutItem::TokenUsageRecord(_) => false,
RolloutItem::Compacted(_) | RolloutItem::EventMsg(_) | RolloutItem::SessionMeta(_) => true,
@@ -936,9 +936,8 @@ impl LocalAgentControl {
} else {
Vec::new()
};
let mut preserve_reference_context_item =
matches!(fork_mode, SpawnAgentForkMode::FullHistory);
if preserve_reference_context_item {
let mut preserve_context_baselines = matches!(fork_mode, SpawnAgentForkMode::FullHistory);
if preserve_context_baselines {
for item in forked_rollout_items.iter().rev() {
let RolloutItem::Compacted(compacted) = item else {
continue;
@@ -946,7 +945,7 @@ impl LocalAgentControl {
// Legacy checkpoints force the child to rebuild context regardless of the
// live parent's reference baseline; an older superseded checkpoint does not.
if compacted.replacement_history.is_none() {
preserve_reference_context_item = false;
preserve_context_baselines = false;
}
break;
}
@@ -1001,7 +1000,7 @@ impl LocalAgentControl {
// If the child will rebuild its initial context, drop the inherited
// instructions; startup will add the current requirements and effort
// instructions once.
return preserve_reference_context_item;
return preserve_context_baselines;
}
let (
Some(parent_developer_instructions),
@@ -1019,7 +1018,7 @@ impl LocalAgentControl {
}
*replaced = true;
let replacement = if preserve_reference_context_item {
let replacement = if preserve_context_baselines {
subagent_developer_instructions.as_str()
} else {
""
@@ -1034,7 +1033,7 @@ impl LocalAgentControl {
true
};
forked_rollout_items.retain_mut(|item| {
if !keep_forked_rollout_item(item, preserve_reference_context_item)
if !keep_forked_rollout_item(item, preserve_context_baselines)
|| destination_history_mode == Some(ThreadHistoryMode::Paginated)
&& matches!(
&*item,
@@ -1054,8 +1053,15 @@ impl LocalAgentControl {
retain_forked_item(response_item, &mut replaced_parent_developer_instructions)
}
RolloutItem::Compacted(compacted) => {
// This checkpoint belongs to the inherited parent prefix.
// This compaction becomes part of the subagent's initial history. Rewrite its
// metadata to describe the child rather than the parent.
compacted.latest_token_usage_record = None;
if let Some(resume_metadata) = &mut compacted.resume_metadata {
resume_metadata.multi_agent_version = Some(multi_agent_version);
if !preserve_context_baselines {
resume_metadata.previous_turn_settings = None;
}
}
// Parent-local review evidence must not become the child's authorization.
// Root user authorization is collected separately by the host.
compacted.guardian_history = None;
@@ -1097,7 +1103,7 @@ impl LocalAgentControl {
// context omitted the parent's developer fragment, append the child's override so its
// instructions still reach the model exactly once.
if let Some(subagent_developer_instructions) = subagent_developer_instructions.as_ref()
&& preserve_reference_context_item
&& preserve_context_baselines
&& !replaced_parent_developer_instructions
&& !subagent_developer_instructions.is_empty()
&& parent_thread
@@ -1111,7 +1117,7 @@ impl LocalAgentControl {
));
forked_rollout_items.push(RolloutItem::ResponseItem(developer_message.into()));
}
if preserve_reference_context_item
if preserve_context_baselines
&& multi_agent_version == MultiAgentVersion::V2
&& let Some(subagent_usage_hint) = options
.multi_agent_v2_usage_hints