Assign stable IDs to generated Responses input items (#41349)

## What changed

- Assign type-prefixed IDs to Guardian classifier input items before sending the request, preserving existing IDs and reusing generated IDs across WebSocket retries.
- Add a prefixed ID to the memory phase-one user message.

## Testing

- Verify retried Guardian requests contain the same unique, prefixed item IDs.
- Verify the memory phase-one request includes a prefixed user-message ID.

GitOrigin-RevId: e01da49d9a661b6bfd2088f091a3ddf69d601e40
This commit is contained in:
pakrym-oai
2026-08-28 15:10:58 +00:00
committed by copyberry
parent 92f887ec35
commit 868c9edb0d
5 changed files with 36 additions and 2 deletions
@@ -1208,6 +1208,7 @@ classifier_instructions = "Predict future violations.\n# Security Policy\n{{ ten
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -1246,6 +1247,7 @@ max_classifier_instruction_tokens = 256
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -1334,6 +1336,7 @@ max_recent_non_user_entries = 8
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -1735,6 +1738,7 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -1900,6 +1904,7 @@ async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -2321,6 +2326,7 @@ async fn contributor_uses_catalog_policy_without_a_configured_override() -> Resu
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -2363,6 +2369,7 @@ async fn contributor_preserves_uncapped_classifier_instructions() -> Result<()>
request["input"][1],
json!({
"type": "message",
"id": request["input"][1]["id"],
"role": "developer",
"content": [{
"type": "input_text",
@@ -29,6 +29,7 @@ use codex_login::default_client::default_headers;
use codex_model_provider::AgentIdentitySessionFallback;
use codex_model_provider::ProviderAuthScope;
use codex_model_provider::SharedModelProvider;
use codex_protocol::ResponseItemId;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::models::ContentItem;
@@ -504,6 +505,14 @@ impl LunaSampler {
phase: None,
internal_chat_message_metadata_passthrough: None,
});
// Assign IDs once so retries reuse the same input item identities.
for item in &mut input {
if item.id().is_none()
&& let Some(prefix) = item.id_prefix()
{
item.set_id(Some(ResponseItemId::new(prefix)));
}
}
let mut request = ResponsesApiRequest {
model: MODEL.to_owned(),
instructions: String::new(),
@@ -904,7 +904,10 @@ async fn sampler_retries_expired_websockets_on_another_warm_connection() -> Resu
))
.await?;
let output = sampler.sample(sample_request("turn-1")).await?;
let mut request = sample_request("turn-1");
request.trusted_review_evidence = vec!["trusted review".to_owned()];
request.trusted_skill_paths = vec!["/skills/review/SKILL.md".to_owned()];
let output = sampler.sample(request).await?;
assert_eq!(output, "low");
let expired_requests = expired.single_connection();
@@ -925,6 +928,13 @@ async fn sampler_retries_expired_websockets_on_another_warm_connection() -> Resu
.remove("client_metadata");
}
assert_eq!(expired_request, healthy_request);
let input: Vec<ResponseItem> = serde_json::from_value(healthy_request["input"].clone())?;
let ids = input
.iter()
.map(|item| item.id().expect("classifier input item ID"))
.collect::<HashSet<_>>();
assert_eq!(ids.len(), input.len());
assert!(ids.iter().all(|id| id.is_prefixed()));
Ok(())
}
+2 -1
View File
@@ -9,6 +9,7 @@ use codex_config::types::MemoriesConfig;
use codex_core::Prompt;
use codex_core::RolloutRecorder;
use codex_core::config::Config;
use codex_protocol::ResponseItemId;
use codex_protocol::error::CodexErr;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
@@ -292,7 +293,7 @@ mod job {
let mut prompt = Prompt::default();
prompt.input = vec![ResponseItem::Message {
id: None,
id: Some(ResponseItemId::new("msg")),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: build_stage_one_input_message(
@@ -19,6 +19,7 @@ use codex_model_provider::ProviderAccountResult;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::ResponseItemId;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::ContentItem;
@@ -689,6 +690,12 @@ async fn memories_startup_phase1_provider_default_drives_request_model() -> anyh
request.body_json()["model"].as_str(),
Some(MOCK_PROVIDER_PHASE_ONE_MODEL)
);
let input: Vec<ResponseItem> = serde_json::from_value(request.body_json()["input"].clone())?;
let message = input
.iter()
.find(|item| item.is_user_message())
.expect("phase-one input message");
assert!(message.id().is_some_and(ResponseItemId::is_prefixed));
Ok(())
}