Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion codex-rs/app-server/tests/suite/v2/thread_inject_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
resumed_history
.history
.iter()
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if response_item == &injected_item)),
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if responses::strip_metadata(response_item.clone()) == injected_item)),
"injected item should be persisted in rollout history"
);

Expand Down
8 changes: 5 additions & 3 deletions codex-rs/app-server/tests/suite/v2/web_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,18 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
search_body["input"]
.as_array()
.context("search input should be an array")?
.last(),
Some(&json!({
.last()
.cloned()
.map(responses::strip_metadata_from_json),
Some(json!({
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Search the web"}],
}))
);

assert_eq!(
requests[1].function_call_output(call_id),
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)),
json!({
"type": "function_call_output",
"call_id": call_id,
Expand Down
24 changes: 14 additions & 10 deletions codex-rs/core/src/agent/control_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,15 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
parent_thread
.inject_user_message_without_turn("parent seed context".to_string())
.await;
let expected_parent_seed = parent_thread
.codex
.session
.clone_history()
.await
.raw_items()
.first()
.cloned()
.expect("parent seed should be recorded");
let turn_context = parent_thread.codex.session.new_default_turn().await;
let parent_spawn_call_id = "spawn-call-history".to_string();
let trigger_message = InterAgentCommunication::new(
Expand Down Expand Up @@ -988,17 +997,12 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
);
assert_ne!(child_thread_id, parent_thread_id);
let history = child_thread.codex.session.clone_history().await;
let mut expected_final_answer =
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer));
expected_final_answer.set_turn_id_if_missing(&turn_context.sub_id);
let expected_history = [
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "parent seed context".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
expected_parent_seed,
expected_final_answer,
ResponseItem::Message {
id: None,
role: "developer".to_string(),
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ async fn run_compact_task_inner_impl(
let user_messages = collect_user_messages(history_items);

let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
if let Some(summary_item) = new_history.last_mut() {
// This replacement history skips `record_conversation_items`; only the appended summary
// belongs to this compaction turn.
summary_item.set_turn_id_if_missing(&turn_context.sub_id);
}
let (window_number, window_ids) = sess.advance_auto_compact_window().await;

if matches!(
Expand Down
4 changes: 1 addition & 3 deletions codex-rs/core/src/compact_remote_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,7 @@ async fn run_remote_compact_task_inner_impl(
)
.await?;
let mut input = prompt_input.clone();
input.push(ResponseItem::CompactionTrigger {
internal_chat_message_metadata_passthrough: None,
});
input.push(ResponseItem::CompactionTrigger {});
let prompt = Prompt {
input,
tools: tool_router.model_visible_specs(),
Expand Down
11 changes: 10 additions & 1 deletion codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,10 @@ impl Session {
) -> Cow<'a, [ResponseItem]> {
let mut items = Cow::Borrowed(items);
prepare_response_items(items.to_mut());
// Most response items get their passthrough turn ID at the durable history boundary.
for item in items.to_mut() {
item.set_turn_id_if_missing(&turn_context.sub_id);
}
if turn_context.config.features.enabled(Feature::ItemIds) {
Self::assign_missing_response_item_ids(items)
} else {
Expand Down Expand Up @@ -2780,8 +2784,9 @@ impl Session {
pub(crate) async fn record_inter_agent_communication(
&self,
turn_context: &TurnContext,
communication: InterAgentCommunication,
mut communication: InterAgentCommunication,
) {
communication.set_turn_id_if_missing(&turn_context.sub_id);
let response_item = communication.to_model_input_item();
let items = self.prepare_conversation_items_for_history(
turn_context,
Expand Down Expand Up @@ -3327,6 +3332,10 @@ impl Session {
{
items.push(guardian_developer_message);
}
// New context windows and compaction install these items directly into replacement history.
for item in &mut items {
item.set_turn_id_if_missing(&turn_context.sub_id);
}
items
}

Expand Down
70 changes: 66 additions & 4 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata_from_items;
use core_test_support::test_codex::local;
use core_test_support::test_codex::test_codex;
use core_test_support::test_path_buf;
Expand Down Expand Up @@ -1692,6 +1693,67 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
assert_eq!(expected, history.raw_items());
}

#[tokio::test]
async fn record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id() {
let (session, turn_context) = make_session_and_context().await;
let fresh_item = user_message("fresh");
let mut existing_item = assistant_message("existing");
existing_item.set_turn_id_if_missing("older-turn");

session
.record_conversation_items(&turn_context, &[fresh_item.clone(), existing_item.clone()])
.await;

let mut expected_fresh_item = fresh_item;
expected_fresh_item.set_turn_id_if_missing(&turn_context.sub_id);
let expected_items = vec![expected_fresh_item, existing_item];
assert_eq!(
session.clone_history().await.raw_items(),
expected_items.as_slice()
);
}

#[tokio::test]
async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
let (mut session, turn_context) = make_session_and_context().await;
let rollout_path = attach_thread_persistence(&mut session).await;
let communication = InterAgentCommunication::new(
AgentPath::root().join("worker").expect("worker path"),
AgentPath::root(),
Vec::new(),
"child done".to_string(),
/*trigger_turn*/ false,
);
let mut expected_item = communication.to_model_input_item();
expected_item.set_turn_id_if_missing(&turn_context.sub_id);

session
.record_inter_agent_communication(&turn_context, communication)
.await;

assert_eq!(
session.clone_history().await.raw_items(),
std::slice::from_ref(&expected_item)
);

session.flush_rollout().await.expect("rollout should flush");
let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path)
.await
.expect("read rollout history")
else {
panic!("expected resumed rollout history");
};

let (resumed_session, _resumed_turn_context) = make_session_and_context().await;
resumed_session
.record_initial_history(InitialHistory::Resumed(resumed))
.await;
assert_eq!(
resumed_session.clone_history().await.raw_items(),
std::slice::from_ref(&expected_item)
);
}

#[tokio::test]
async fn prepares_image_failures_before_history_insertion() {
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
Expand Down Expand Up @@ -1758,7 +1820,7 @@ async fn prepares_image_failures_before_history_insertion() {
},
internal_chat_message_metadata_passthrough: None,
}];
assert_eq!(history.raw_items(), expected.as_slice());
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
}

#[tokio::test]
Expand Down Expand Up @@ -8207,7 +8269,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
),
);
let expected = vec![image_message, item];
assert_eq!(history.raw_items(), expected.as_slice());
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
assert_eq!(
std::fs::read(&expected_saved_path).expect("saved file"),
b"foo"
Expand Down Expand Up @@ -8250,7 +8312,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {

let history = session.clone_history().await;
let expected = vec![item];
assert_eq!(history.raw_items(), expected.as_slice());
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
assert!(!expected_saved_path.exists());
}

Expand Down Expand Up @@ -9000,7 +9062,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
internal_chat_message_metadata_passthrough: None,
};
assert!(
history.raw_items().iter().any(|item| item == &expected),
strip_metadata_from_items(history.raw_items()).contains(&expected),
"expected pending input to be persisted into history on turn completion"
);

Expand Down
6 changes: 4 additions & 2 deletions codex-rs/core/src/tools/handlers/extension_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,13 @@ mod tests {
session
.record_conversation_items(&turn, std::slice::from_ref(&history_item))
.await;
let mut expected_history_item = history_item.clone();
expected_history_item.set_turn_id_if_missing(&turn_id);
let raw_history_event = rx.recv().await.expect("history raw response item event");
let EventMsg::RawResponseItem(raw_history_item) = raw_history_event.msg else {
panic!("expected raw response item event");
};
assert_eq!(raw_history_item.item, history_item);
assert_eq!(raw_history_item.item, expected_history_item);
let invocation = ToolInvocation {
session,
turn,
Expand Down Expand Up @@ -377,7 +379,7 @@ mod tests {
);
assert_eq!(
captured_call.conversation_history.items(),
std::slice::from_ref(&history_item)
std::slice::from_ref(&expected_history_item)
);
match captured_call.payload {
ToolPayload::Function { arguments } => {
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/src/tools/router_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
session
.record_conversation_items(&turn, std::slice::from_ref(&history_item))
.await;
let mut expected_history_item = history_item.clone();
expected_history_item.set_turn_id_if_missing(&turn.sub_id);

let router = ToolRouter::from_turn_context(
&turn,
Expand Down Expand Up @@ -404,7 +406,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
json!({
"arguments": { "message": "hello" },
"callId": "call-extension",
"conversationHistory": [history_item],
"conversationHistory": [expected_history_item],
"ok": true,
})
);
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/tests/common/context_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Loading
Loading