mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Retain assistant context for Guardian authorization reviews (#47582)
## Why Ordinary user replies can depend on preceding assistant messages. Losing those messages during compaction can leave Guardian without the context needed to interpret a reply's scope. ## What changed - Retain original assistant messages, including commentary, with their recorded order across checkpoints and resume. Exclude compaction output from this evidence. - Include assistant context in local and delegated reviews as untrusted context, with separate storage limits and lower budget priority than user instructions. - Prefer assistant context preceding ordinary replies when selecting bounded root evidence, and report unavailable context without inferring what a reply refers to. - Preserve inherited context scope and remove assistant evidence beyond rollback boundaries, including during rollout migration. ## Testing Extend coverage for compaction and resume, checkpoint migration, queued-input ordering and rollback, role labeling, and evidence budgets that preserve user restrictions while dropping assistant context. GitOrigin-RevId: 886189b0be29ffc3d324b56f5d0b673d6b8d84da
This commit is contained in:
@@ -64,6 +64,7 @@ impl LocalAgentRuntime {
|
||||
RetainedContextEntry::UserMessage(message),
|
||||
) => Some(message.complete.then(|| message.text.clone())),
|
||||
(RetainedContextOrder::Inherited(_), _)
|
||||
| (_, RetainedContextEntry::AssistantMessage(_))
|
||||
| (_, RetainedContextEntry::VerifiedAnswer(_)) => None,
|
||||
})
|
||||
.rev()
|
||||
|
||||
@@ -988,9 +988,9 @@ impl LocalAgentControl {
|
||||
let retain_forked_item = |envelope: &mut ResponseItemEnvelope, replaced: &mut bool| {
|
||||
if context_mode == GuardianContextMode::ThreadOwned
|
||||
&& multi_agent_version == MultiAgentVersion::V2
|
||||
&& matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "user")
|
||||
&& matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "user" || role == "assistant")
|
||||
{
|
||||
// Persist the scope of every inherited user message, including the suffix
|
||||
// Persist the scope of every inherited conversational message, including the suffix
|
||||
// after a checkpoint. Resume must not recapture it as local authorization.
|
||||
envelope
|
||||
.metadata
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Projects bounded root evidence for worker reviewers using the session's capture policy.
|
||||
//! Retained root instructions stay authoritative while old checkpoints use legacy review.
|
||||
//! User evidence wins, then assistant context preceding ordinary replies, then other live evidence.
|
||||
//! Thread-owned recovery includes commentary; legacy projection keeps its phase filter.
|
||||
//! Projection limits do not change authorization completeness; unavailable source text does.
|
||||
//! Retained-history reconciliation owns recovery order and missing-instruction provenance.
|
||||
//! Known positions preserve host order, not delivery order or inferred question-answer pairs.
|
||||
@@ -163,39 +165,114 @@ impl LocalAgentControl {
|
||||
codex_guardian_context::render_verified_answer(answer)
|
||||
.map(|text| (Some(order), GuardianRootMessage::UserInput(text)))
|
||||
}
|
||||
RetainedContextEntry::AssistantMessage(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
messages.drain(..messages.len().saturating_sub(MAX_ROOT_MESSAGES));
|
||||
// Optional assistant context cannot evict required grants or restrictions.
|
||||
let mut assistant_messages = root_history
|
||||
let mut missing_assistant_context = retained_context.has_omitted_assistant_messages();
|
||||
// Prefer a renderable retained original over a shortened checkpoint copy.
|
||||
// Otherwise preserve the bounded live evidence, including confirmed messaging sends.
|
||||
let live_assistant_messages = root_history
|
||||
.annotated_items()
|
||||
.iter()
|
||||
.filter(|envelope| {
|
||||
!envelope
|
||||
.metadata
|
||||
.as_ref()
|
||||
.is_some_and(|metadata| metadata.compaction_output)
|
||||
})
|
||||
.filter_map(|envelope| {
|
||||
let text = root_assistant_text(&envelope.item, &delivered_messages)?;
|
||||
let id = envelope
|
||||
.item
|
||||
.id()
|
||||
.map(codex_protocol::ResponseItemId::as_str);
|
||||
let retained = reconciled.ordered_entries().find_map(|(order, entry)| {
|
||||
let RetainedContextEntry::AssistantMessage(message) = entry else {
|
||||
return None;
|
||||
};
|
||||
id.filter(|id| message.message_id.as_deref() == Some(*id))
|
||||
.map(|_| (order, message))
|
||||
});
|
||||
if let Some((order, message)) = retained
|
||||
&& let Some(message) =
|
||||
codex_guardian_context::retained_assistant_message(message)
|
||||
{
|
||||
return Some((id, Some(order), message));
|
||||
}
|
||||
let order = envelope
|
||||
.metadata
|
||||
.as_ref()
|
||||
.filter(|metadata| !metadata.inherited_user_message)
|
||||
.and_then(|metadata| metadata.user_input_order)
|
||||
.map(RetainedContextOrder::Local);
|
||||
let order = retained.map(|(order, _)| order).or(order);
|
||||
let message = if order.is_some() {
|
||||
GuardianRootMessage::Assistant(text)
|
||||
} else {
|
||||
GuardianRootMessage::UnorderedAssistant(text)
|
||||
};
|
||||
Some((order, message))
|
||||
Some((id, order, message))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut assistant_messages = reconciled
|
||||
.ordered_entries()
|
||||
.filter_map(|(order, entry)| {
|
||||
let RetainedContextEntry::AssistantMessage(message) = entry else {
|
||||
return None;
|
||||
};
|
||||
if message.message_id.as_deref().is_some_and(|id| {
|
||||
live_assistant_messages
|
||||
.iter()
|
||||
.any(|(live_id, _, _)| *live_id == Some(id))
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
let rendered = codex_guardian_context::retained_assistant_message(message);
|
||||
missing_assistant_context |= rendered.is_none();
|
||||
rendered.map(|message| (Some(order), message))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assistant_messages.extend(
|
||||
live_assistant_messages
|
||||
.into_iter()
|
||||
.map(|(_, order, message)| (order, message)),
|
||||
);
|
||||
// Keep the nearest known assistant context for each ordinary reply. Verified
|
||||
// answers already include their questions; unsequenced sources cannot be paired.
|
||||
let reply_context_orders = messages
|
||||
.iter()
|
||||
.filter_map(|(user_order, message)| {
|
||||
let (Some(user_order), GuardianRootMessage::User(_)) = (user_order, message)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
assistant_messages
|
||||
.iter()
|
||||
.filter_map(|(order, _)| *order)
|
||||
.filter(|order| order < user_order)
|
||||
.max()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Stable sorting keeps other live evidence ahead of retained-only extras.
|
||||
assistant_messages.sort_by_key(|(order, _)| {
|
||||
order.filter(|order| reply_context_orders.contains(order))
|
||||
});
|
||||
let available = MAX_ROOT_MESSAGES.saturating_sub(messages.len());
|
||||
missing_assistant_context |= assistant_messages.len() > available;
|
||||
assistant_messages.drain(..assistant_messages.len().saturating_sub(available));
|
||||
messages.extend(assistant_messages);
|
||||
// Select evidence by priority first, then restore recorded order. Older assistant
|
||||
// context has no comparable position and is explicitly labeled at the end.
|
||||
messages.sort_by_key(|(order, _)| (order.is_none(), *order));
|
||||
let mut messages = messages
|
||||
.into_iter()
|
||||
.map(|(_, message)| message)
|
||||
.collect::<Vec<_>>();
|
||||
if missing_assistant_context {
|
||||
messages.insert(
|
||||
/*index*/ 0,
|
||||
GuardianRootMessage::IncompleteAssistantContext,
|
||||
);
|
||||
}
|
||||
let mut authorization_version = root_evidence.authorization_version(history.as_ref());
|
||||
if !authorization_version.retained_context_complete {
|
||||
messages.insert(
|
||||
@@ -231,6 +308,11 @@ impl LocalAgentControl {
|
||||
(_, ResponseItem::FunctionCall { call_id, .. }) => root_evidence
|
||||
.user_input_for_call(history.as_ref(), call_id)
|
||||
.map(GuardianRootMessage::UserInput),
|
||||
(Some(TurnItem::AgentMessage(message)), _)
|
||||
if !matches!(message.phase, None | Some(MessagePhase::FinalAnswer)) =>
|
||||
{
|
||||
None
|
||||
}
|
||||
_ => root_assistant_text(item, &delivered_messages)
|
||||
.map(GuardianRootMessage::Assistant),
|
||||
})
|
||||
@@ -268,9 +350,6 @@ fn root_assistant_text(
|
||||
let Some(TurnItem::AgentMessage(message)) = parse_turn_item(item) else {
|
||||
return None;
|
||||
};
|
||||
if !matches!(message.phase, None | Some(MessagePhase::FinalAnswer)) {
|
||||
return None;
|
||||
}
|
||||
message
|
||||
.content
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user