mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Add per-item retention policies for injected history (#61344)
## What changed Allow `thread/inject_items` to accept wrapped items with `retention: "default"` or `retention: "always"`, while preserving support for raw response items. Persist explicit policies separately from legacy client-authored metadata. Carry explicitly retained items through local and remote compaction, context-window changes, and child-agent creation, subject to retention budgets. Preserve history order and keep retained tool exchanges together; require matching calls and outputs to be adjacent in the injection request and both marked `always`. Update Quinn dependencies and record exceptions for additional Hickory DNSSEC advisories where DNSSEC is disabled. ## Testing Update injection tests for explicit retention metadata and extend the compaction scenario to cover a retained tool exchange across repeated compaction and restart. GitOrigin-RevId: 1195a4ed62dc162a62e58bc2ad733262fbabea27
This commit is contained in:
@@ -62,6 +62,8 @@ use codex_protocol::protocol::TurnEnvironmentRequests;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::turn_input::InjectedResponseItem;
|
||||
use codex_protocol::turn_input::ItemRetention;
|
||||
use codex_protocol::turn_input::RecoverTurnRequest;
|
||||
use codex_protocol::turn_input::StartIfIdleSubmission;
|
||||
use codex_protocol::turn_input::SteerSubmission;
|
||||
@@ -1032,7 +1034,15 @@ impl CodexThread {
|
||||
|
||||
/// Record raw Responses API items without starting a new turn.
|
||||
pub async fn inject_response_items(&self, items: Vec<ResponseItem>) -> CodexResult<()> {
|
||||
self.inject_response_items_for_turn(items).await?;
|
||||
self.inject_response_items_with_retention(items).await
|
||||
}
|
||||
|
||||
/// Inject Responses items with explicit, persisted retention policies.
|
||||
pub async fn inject_response_items_with_retention(
|
||||
&self,
|
||||
items: Vec<impl Into<InjectedResponseItem>>,
|
||||
) -> CodexResult<()> {
|
||||
self.record_injected_items(items).await?;
|
||||
self.checkpoint_preparation().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1058,37 +1068,43 @@ impl CodexThread {
|
||||
pub async fn inject_response_items_for_turn(
|
||||
&self,
|
||||
items: Vec<ResponseItem>,
|
||||
) -> CodexResult<()> {
|
||||
self.record_injected_items(items).await
|
||||
}
|
||||
|
||||
async fn record_injected_items(
|
||||
&self,
|
||||
items: Vec<impl Into<InjectedResponseItem>>,
|
||||
) -> CodexResult<()> {
|
||||
if items.is_empty() {
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"items must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
items.iter().try_for_each(validate_injected_response_item)?;
|
||||
|
||||
let had_reference_context = self.session.reference_context_item().await.is_some();
|
||||
let mut turn_context = if had_reference_context {
|
||||
self.session.new_inject_items_context().await
|
||||
} else {
|
||||
self.session.new_default_turn().await
|
||||
};
|
||||
if self.session.reference_context_item().await.is_none() {
|
||||
// Compaction can clear the reference while the recording context is built.
|
||||
// Initial context must capture a step with a complete skills snapshot.
|
||||
if had_reference_context {
|
||||
turn_context = self.session.new_default_turn().await;
|
||||
let items: Vec<InjectedResponseItem> = items.into_iter().map(Into::into).collect();
|
||||
let mut indexed_items = items.iter().enumerate();
|
||||
while let Some((index, item)) = indexed_items.next() {
|
||||
validate_injected_response_item(&item.item)?;
|
||||
if item.retention != ItemRetention::Always {
|
||||
continue;
|
||||
}
|
||||
let needs_call = crate::tools::output_call_id(&item.item).is_some()
|
||||
&& !matches!(&item.item, ResponseItem::ToolSearchOutput { execution, .. } if execution == "server");
|
||||
if (crate::tools::input_call_id(&item.item).is_some() || needs_call)
|
||||
&& !indexed_items.next().is_some_and(|(_, output)| {
|
||||
output.retention == ItemRetention::Always
|
||||
&& crate::tools::is_matching_tool_output(&item.item, &output.item)
|
||||
})
|
||||
{
|
||||
return Err(CodexErr::InvalidRequest(format!(
|
||||
"items[{index}]: retained tool calls and outputs must be adjacent in the same request, call first, with both marked always"
|
||||
)));
|
||||
}
|
||||
// This history-only API runs without run_turn, so it owns its initial step.
|
||||
let step_context = self
|
||||
.session
|
||||
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
|
||||
.await?;
|
||||
self.session
|
||||
.record_context_updates_and_set_reference_context_item(step_context.as_ref())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let turn_context = self.session.history_injection_context().await?;
|
||||
self.session
|
||||
.inject_client_response_items(items, turn_context.as_ref())
|
||||
.inject_client_response_items(items, &turn_context)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user