Retain inherited Guardian instructions in standalone forks (#43478)

## Why

A standalone root forked from a worker needs its inherited user instructions for Guardian review, including after compaction. Parent input counters belong to a different thread, and omitted parent answers may have restricted an inherited authorization.

## What changed

- Adopt inherited user instructions into thread-owned retained context for standalone roots, recovering surviving checkpoint messages when needed.
- Keep inherited instructions before local evidence without sharing local acceptance counters, and preserve that distinction during reconciliation and rollback.
- Mark parent verified answers as incomplete and treat recovered checkpoint instructions as incomplete excerpts.

## Testing

Add coverage for standalone forks from copied history and truncated checkpoints, retention through compaction and resume, inherited/local ordering collisions, and rollback scope.

GitOrigin-RevId: bf716e224a1d29d6c528ef0dd7cdb118a413a08c
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-09-07 13:58:57 +00:00
1 parent aa12ab45df
commit 16ff14c266
19 files changed
+646 -131

No files matched your search

@@ -17,6 +17,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::UserMessageEvent;
use codex_rollout::CompactedItem;
use codex_rollout::RetainedContextEntry;
use codex_rollout::RetainedInputSource;
use codex_rollout::RolloutItem;
use codex_rollout::RolloutLine;
@@ -42,7 +43,7 @@ struct PendingUserResponse {
struct InstructionBoundary {
record_index: usize,
message_id: Option<ResponseItemId>,
acceptance_order: Option<u64>,
input_source: RetainedInputSource,
alive: bool,
}
@@ -161,10 +162,7 @@ impl RollbackPlanner {
} else if rollback::counts_as_boundary(&response.item) {
let boundary = self.start_boundary(index);
self.boundaries[boundary].message_id = response.id().cloned();
self.boundaries[boundary].acceptance_order = response
.metadata
.as_ref()
.and_then(|metadata| metadata.user_input_order);
self.boundaries[boundary].input_source = response.metadata.as_ref().into();
if let ResponseItem::Message { role, content, .. } = &response.item
&& role == "user"
{
@@ -323,7 +321,7 @@ impl RollbackPlanner {
self.boundaries.push(InstructionBoundary {
record_index: index,
message_id: None,
acceptance_order: None,
input_source: RetainedInputSource::Local(None),
alive: true,
});
let had_prior_boundary = !self.boundary_stack.is_empty();
@@ -380,7 +378,8 @@ impl RollbackPlanner {
}
if let Some(boundary) = first_removed_boundary {
let source = &self.boundaries[boundary];
if let Some(order) = source.acceptance_order {
let acceptance_order = source.input_source.acceptance_order();
if let Some(order) = acceptance_order {
// An answer may have been persisted before the queued instruction
// accepted ahead of it. Both are removed at that acceptance boundary.
for fact in &self.retained_fact_sources {
@@ -411,10 +410,10 @@ impl RollbackPlanner {
// including in checkpoints. Legacy evidence uses the recorded boundary.
// Later checkpoints have not been observed yet and already reflect this rollback.
for frame in self.compactions.iter_mut().rev().take_while(|frame| {
source.acceptance_order.is_some() || frame.record_index >= source.record_index
acceptance_order.is_some() || frame.record_index >= source.record_index
}) {
if let Some(context) = &mut frame.item.retained_context {
if source.acceptance_order.is_some()
if acceptance_order.is_some()
|| context
.ordered_entries()
.any(|(_, entry)| matches!(entry, RetainedContextEntry::UserMessage(_)))
@@ -422,7 +421,7 @@ impl RollbackPlanner {
context.rollback(
&removed_turns,
source.message_id.as_ref().map(ResponseItemId::as_str),
source.acceptance_order,
source.input_source,
);
} else {
// Checkpoints written without instruction retention keep the legacy