Preserve Guardian history across thread reconstruction (#42065)

## Why

Guardian review evidence must remain available after a compacted thread is resumed or forked, while rolled-back or parent-local evidence must not grant authorization in a different context.

## What changed

- Store the bounded, model-invisible Guardian transcript alongside compacted rollout history and restore it from the newest surviving checkpoint during replay.
- Trim Guardian history at rollback boundaries, clearing it when the boundary has already been evicted.
- Preserve the checkpoint for user-initiated forks, but remove it when spawning a subagent so parent review evidence is not inherited.
- Keep the new rollout field optional for compatibility with existing compacted records and legacy readers.

## Testing

- Cover Guardian history across compaction, restart, paginated and pathless stores, user forks, rollback, bounded replay, serialization, and subagent forks.

GitOrigin-RevId: 6ab076d5e0c3aac4d6a41ebd50ba2b200c90213e
This commit is contained in:
jif authored and copyberry committed 2026-09-01 14:33:58 +00:00
1 parent 3a04482645
commit 28097e98eb
23 files changed
+369 -27

No files matched your search

+14 -1
View File
@@ -1,7 +1,7 @@
//! Bounded, chronological review evidence independent of the parent's compaction.
//!
//! User messages and other entries have separate limits and keep their original order.
//! Hosts append original items and reset this history on rollback or reconstruction.
//! Hosts append original items, restore bounded checkpoints, and trim rolled-back turns.
//! Clones share immutable payloads; eviction changes the generation so readers cannot
//! reuse an offset into a different retained prefix. Prompt selection remains caller-owned.
@@ -142,6 +142,19 @@ impl TranscriptHistory {
}
}
/// Removes a rolled-back boundary and everything after it. If retention already
/// evicted the boundary, discard the remaining evidence rather than keep later grants.
pub fn truncate_before(&mut self, boundary: &ResponseItem) {
let index = self.items.iter().position(|(item, _)| {
item.id()
.zip(boundary.id())
.is_some_and(|(item_id, boundary_id)| item_id == boundary_id)
|| item.as_ref() == boundary
});
self.items.truncate(index.unwrap_or(0));
self.generation = self.generation.saturating_add(1);
}
/// Generation of the retained prefix, independent of parent compaction.
pub fn generation(&self) -> u64 {
self.generation