Move Guardian review state into the reviewer extension (#57697)

## What changed

Move review state, fork history, checkpoint recovery bookkeeping, and Node REPL evidence admission into `codex-guardian-reviewer`. Expose helpers for session initialization and snapshot publication, and update core to delegate to them while preserving the existing review logic.

GitOrigin-RevId: 56d6a0124301a3c70908614d4bc3bfe970ac1c85
This commit is contained in:
jif
2026-10-28 11:01:32 +00:00
committed by copyberry
parent f021168bde
commit d09a5cd8cf
9 changed files with 205 additions and 137 deletions
@@ -18,6 +18,7 @@ codex-analytics = { workspace = true }
codex-extension-api = { workspace = true }
codex-feedback = { workspace = true }
codex-guardian-context = { workspace = true }
codex-history = { workspace = true }
codex-mcp = { workspace = true }
codex-otel = { workspace = true }
codex-prompts = { workspace = true }
+10
View File
@@ -25,6 +25,15 @@ mod prompt;
mod reporting;
mod retry;
mod review;
mod review_state;
pub use review_state::CheckpointRecovery;
pub use review_state::GuardianReviewForkSnapshot;
pub use review_state::GuardianReviewHistory;
pub use review_state::GuardianReviewState;
pub use review_state::GuardianTranscriptSource;
pub use review_state::PendingNodeReplEvidenceAdmission;
pub use review_state::ReviewerSelection;
pub use review_state::admit_node_repl_evidence;
mod routing;
mod session;
mod settings;
@@ -68,6 +77,7 @@ pub use pool::ReviewerRequest;
pub use pool::ReviewerSession;
pub use pool::ReviewerTasks;
pub use pool::SessionDisposition;
pub use prompt::GUARDIAN_TRANSCRIPT_START;
pub use prompt::GuardianPromptItems;
pub use prompt::compose_guardian_prompt;
@@ -9,6 +9,8 @@ use codex_guardian_context::TranscriptCursor as GuardianTranscriptCursor;
use codex_guardian_context::TranscriptMode as GuardianPromptMode;
use codex_guardian_context::TranscriptSelection;
pub const GUARDIAN_TRANSCRIPT_START: &str = ">>> TRANSCRIPT START\n";
pub struct GuardianPromptItems {
pub context: ComposedContext,
pub transcript_cursor: GuardianTranscriptCursor,
@@ -0,0 +1,168 @@
//! Owns reviewer transcript progress, fork history, and checkpoint recovery bookkeeping.
//! The host captures history and validates authorization before publishing a checkpoint.
//! Evidence advances only after the matching review turn records its transcript input.
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use codex_analytics::GuardianReviewSessionKind;
use codex_history::InitialHistory;
use codex_history::RolloutItem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use tokio::sync::Mutex;
use crate::ConversationCheckpoint;
use crate::ConversationState;
use crate::GUARDIAN_TRANSCRIPT_START;
/// Opaque conversation progress retained while ThreadManager starts a reviewer.
pub struct GuardianReviewState {
pub conversation: ConversationState<GuardianReviewHistory>,
// Only the first attempt may use the checkpoint loaded without prior reviewer history.
pub fresh_parent_checkpoint: bool,
// The cursor describes evidence admitted before any later reviewer compaction.
pub transcript_history_version: u64,
pub transcript_source: Option<GuardianTranscriptSource>,
pub last_admitted_node_repl_response_sequence: u64,
pub pending_node_repl_evidence_admission: Option<PendingNodeReplEvidenceAdmission>,
}
// These histories have independent version counters; a cursor must identify its source.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GuardianTranscriptSource {
Retained,
LoadedParentCheckpoint,
}
pub struct PendingNodeReplEvidenceAdmission {
pub turn_id: String,
pub response_sequence: u64,
}
pub type GuardianReviewForkSnapshot = ConversationCheckpoint<GuardianReviewHistory>;
/// Host-owned history and admitted evidence used to seed a private reviewer fork.
#[derive(Clone)]
pub struct GuardianReviewHistory {
pub initial_history: InitialHistory,
transcript_source: Option<GuardianTranscriptSource>,
last_admitted_node_repl_response_sequence: u64,
}
/// Controls whether selection may reuse a session or must start from the parent checkpoint.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ReviewerSelection {
ReuseIfAvailable,
FreshParentCheckpoint,
}
/// Requests one bounded restart from the captured parent checkpoint when
/// reviewer compaction invalidates the pending input. The flag belongs to this attempt.
pub struct CheckpointRecovery {
pub requested: Arc<AtomicBool>,
pub history_version: u64,
pub fresh_parent_checkpoint: bool,
}
impl GuardianReviewState {
/// Restores fork progress or starts from the captured parent checkpoint.
pub fn for_session(
kind: GuardianReviewSessionKind,
snapshot: Option<GuardianReviewForkSnapshot>,
reviewer_selection: ReviewerSelection,
parent_compaction: Option<&ResponseItem>,
) -> (Self, Option<InitialHistory>) {
let mut conversation = snapshot
.filter(|_| reviewer_selection == ReviewerSelection::ReuseIfAvailable)
.map(ConversationState::from)
.unwrap_or_default();
if parent_compaction.is_some() && conversation.cursor().is_none() {
// A compacted fork has no valid transcript progress. Prefer the
// captured parent checkpoint to its lossy reviewer summary.
conversation = ConversationState::default();
}
let history = match kind {
GuardianReviewSessionKind::EphemeralForked => conversation.take_history(),
// Replacement trunks keep the checkpoint if their first review is invalidated.
GuardianReviewSessionKind::TrunkNew | GuardianReviewSessionKind::TrunkReused => {
conversation
.snapshot()
.map(|checkpoint| checkpoint.history().clone())
}
};
let state = Self {
conversation,
fresh_parent_checkpoint: history.is_none() && parent_compaction.is_some(),
transcript_history_version: 0,
transcript_source: history
.as_ref()
.and_then(|history| history.transcript_source),
last_admitted_node_repl_response_sequence: history.as_ref().map_or(0, |history| {
history.last_admitted_node_repl_response_sequence
}),
pending_node_repl_evidence_admission: None,
};
let initial_history = history.map(|history| history.initial_history).or_else(|| {
parent_compaction
.cloned()
.map(|item| InitialHistory::Forked(vec![RolloutItem::ResponseItem(item.into())]))
});
(state, initial_history)
}
/// Publishes captured history after the host's final authorization validation.
pub fn commit_snapshot(
&mut self,
items: Vec<RolloutItem>,
history_version: u64,
checkpoint_recovery: Option<&CheckpointRecovery>,
) {
// Forks must not inherit a cursor for evidence the completed turn compacted away.
if self.transcript_history_version != history_version && checkpoint_recovery.is_some() {
self.conversation.reset_transcript();
self.transcript_source = None;
self.last_admitted_node_repl_response_sequence = 0;
}
let last_admitted_node_repl_response_sequence =
self.last_admitted_node_repl_response_sequence;
let transcript_source = self.transcript_source;
self.conversation.commit_snapshot(GuardianReviewHistory {
initial_history: InitialHistory::Forked(items),
transcript_source,
last_admitted_node_repl_response_sequence,
});
}
}
pub async fn admit_node_repl_evidence(state: &Mutex<GuardianReviewState>, event: &Event) {
// Annotated review inputs are recorded as response items without UI turn
// items. Both input paths emit this event after history admission.
let EventMsg::RawResponseItem(recorded) = &event.msg else {
return;
};
let ResponseItem::Message { role, content, .. } = &recorded.item else {
return;
};
if role != "user"
|| !content.iter().any(|item| {
matches!(item, ContentItem::InputText { text }
if text == GUARDIAN_TRANSCRIPT_START || text == ">>> TRANSCRIPT DELTA START\n")
})
{
return;
}
let mut state = state.lock().await;
let Some(pending) = state.pending_node_repl_evidence_admission.as_ref() else {
return;
};
if event.id == pending.turn_id {
state.last_admitted_node_repl_response_sequence = state
.last_admitted_node_repl_response_sequence
.max(pending.response_sequence);
state.pending_node_repl_evidence_admission = None;
}
}