feat(core): store turn_id on ResponseItem metadata (#28360)

## Description

This PR is a followup to https://github.com/openai/codex/pull/28355 and
starts assigning `internal_chat_message_metadata_passthrough.turn_id` to
durable Responses API items created during a turn.

The goal is that those items keep the `turn_id` that introduced them
when Codex resends stateless HTTP context, reconstructs history for
resume/fork paths, or reuses websocket response state.

## What changed

- Set `internal_chat_message_metadata_passthrough.turn_id` when missing
as response items enter durable history, initial/replacement history,
inter-agent communication history, and local compaction summaries.
- Preserve existing item turn IDs instead of overwriting them during
persistence, resume reconstruction, compaction, forked history, and
websocket incremental reuse.
- Keep `compaction_trigger` fieldless because it is a request control,
not a durable response item.
- Update focused history/request assertions and fixtures for stateless
requests, websocket incrementals, compaction, thread injection, prompt
debug, and related CI coverage.
This commit is contained in:
Owen Lin
2026-06-22 16:45:14 -07:00
committed by GitHub
parent 7153affa0f
commit 4a82ecc3c9
27 changed files with 317 additions and 167 deletions
@@ -266,7 +266,7 @@ fn format_request_body_snapshot(
request: &ResponsesRequest,
options: &ContextSnapshotOptions,
) -> String {
let mut body = request.body_json();
let mut body = crate::responses::strip_metadata_from_json(request.body_json());
canonicalize_json_snapshot_value(&mut body, options);
serde_json::to_string_pretty(&body).expect("request body should serialize")
}
+38 -3
View File
@@ -98,6 +98,35 @@ fn decode_body_bytes(body: &[u8], content_encoding: Option<&str>) -> Vec<u8> {
}
}
/// Returns a response item without internal transport metadata for semantic assertions.
pub fn strip_metadata(mut item: ResponseItem) -> ResponseItem {
item.clear_internal_chat_message_metadata_passthrough();
item
}
/// Returns response items without internal transport metadata for semantic assertions.
pub fn strip_metadata_from_items(items: &[ResponseItem]) -> Vec<ResponseItem> {
items.iter().cloned().map(strip_metadata).collect()
}
/// Returns JSON without internal transport metadata for semantic assertions.
pub fn strip_metadata_from_json(value: Value) -> Value {
match value {
Value::Array(values) => {
Value::Array(values.into_iter().map(strip_metadata_from_json).collect())
}
Value::Object(mut map) => {
map.remove("internal_chat_message_metadata_passthrough");
Value::Object(
map.into_iter()
.map(|(key, value)| (key, strip_metadata_from_json(value)))
.collect(),
)
}
value => value,
}
}
impl ResponsesRequest {
pub fn body_json(&self) -> Value {
let body = decode_body_bytes(
@@ -1092,11 +1121,17 @@ pub async fn mount_compact_user_history_with_summary_sequence(
)
})
.collect::<Vec<Value>>();
// Append a synthetic compaction item as the newest item.
output.push(serde_json::json!({
let compaction_turn_id = body_json["client_metadata"]["turn_id"].as_str();
// Match Responses API: generated compaction items inherit the compact request turn.
let mut compaction_item = serde_json::json!({
"type": "compaction",
"encrypted_content": summary_text,
}));
});
if let Some(turn_id) = compaction_turn_id {
compaction_item["internal_chat_message_metadata_passthrough"] =
serde_json::json!({ "turn_id": turn_id });
}
output.push(compaction_item);
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_json(serde_json::json!({ "output": output }))