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:
Vivian Fang
2026-11-13 18:46:13 +00:00
committed by copyberry
parent d17887a8d1
commit c33002259e
29 changed files with 578 additions and 300 deletions
+39 -23
View File
@@ -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(())
}