diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 8f3afaea9b..c63b5b9496 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -245,10 +245,72 @@ fn message_text_for_shape(item: &Value) -> String { .unwrap_or_else(|| "".to_string()) } +fn is_non_real_user_text(text: &str) -> bool { + text == "" + || text == "" + || text == "" + || text.starts_with(" bool { + text == "" + || text == "" + || text.starts_with(" Option { + if item.get("type").and_then(Value::as_str) != Some("message") { + return None; + } + if item.get("role").and_then(Value::as_str) != Some("user") { + return None; + } + + item.get("content") + .and_then(Value::as_array) + .and_then(|content| { + content + .iter() + .filter_map(|entry| entry.get("text").and_then(Value::as_str)) + .map(normalize_shape_text) + .find(|text| !is_non_real_user_text(text)) + }) +} + +fn previous_user_label_for_shape(items: &[Value], idx: usize) -> Option { + items[..idx] + .iter() + .rev() + .find_map(user_message_label_for_shape) +} + +fn next_user_label_for_shape(items: &[Value], idx: usize) -> Option { + items + .iter() + .skip(idx.saturating_add(1)) + .find_map(user_message_label_for_shape) +} + +fn context_anchor_suffix(items: &[Value], idx: usize, role: &str, text: &str) -> Option { + let is_context_message = if role == "developer" { + true + } else { + role == "user" && is_shape_context_item_text(text) + }; + if !is_context_message { + return None; + } + + let prev = previous_user_label_for_shape(items, idx).unwrap_or_else(|| "".to_string()); + let next = next_user_label_for_shape(items, idx).unwrap_or_else(|| "".to_string()); + Some(format!(" [prev_user={prev};next_user={next}]")) +} + fn request_input_shape(request: &ResponsesRequest) -> String { - request - .input() - .into_iter() + let input = request.input(); + input + .iter() .enumerate() .map(|(idx, item)| { let Some(item_type) = item.get("type").and_then(Value::as_str) else { @@ -257,8 +319,9 @@ fn request_input_shape(request: &ResponsesRequest) -> String { match item_type { "message" => { let role = item.get("role").and_then(Value::as_str).unwrap_or("unknown"); - let text = message_text_for_shape(&item); - format!("{idx:02}:message/{role}:{text}") + let text = message_text_for_shape(item); + let suffix = context_anchor_suffix(&input, idx, role, &text).unwrap_or_default(); + format!("{idx:02}:message/{role}:{text}{suffix}") } "function_call" => { let name = item.get("name").and_then(Value::as_str).unwrap_or("unknown"); diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index a8cf41468a..dcc8a1e3ac 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -113,10 +113,71 @@ fn message_text_for_shape(item: &Value) -> String { .unwrap_or_else(|| "".to_string()) } +fn is_non_real_user_text(text: &str) -> bool { + text == "" + || text == "" + || text.starts_with(" bool { + text == "" + || text == "" + || text.starts_with(" Option { + if item.get("type").and_then(Value::as_str) != Some("message") { + return None; + } + if item.get("role").and_then(Value::as_str) != Some("user") { + return None; + } + + item.get("content") + .and_then(Value::as_array) + .and_then(|content| { + content + .iter() + .filter_map(|entry| entry.get("text").and_then(Value::as_str)) + .map(normalize_shape_text) + .find(|text| !is_non_real_user_text(text)) + }) +} + +fn previous_user_label_for_shape(items: &[Value], idx: usize) -> Option { + items[..idx] + .iter() + .rev() + .find_map(user_message_label_for_shape) +} + +fn next_user_label_for_shape(items: &[Value], idx: usize) -> Option { + items + .iter() + .skip(idx.saturating_add(1)) + .find_map(user_message_label_for_shape) +} + +fn context_anchor_suffix(items: &[Value], idx: usize, role: &str, text: &str) -> Option { + let is_context_message = if role == "developer" { + true + } else { + role == "user" && is_shape_context_item_text(text) + }; + if !is_context_message { + return None; + } + + let prev = previous_user_label_for_shape(items, idx).unwrap_or_else(|| "".to_string()); + let next = next_user_label_for_shape(items, idx).unwrap_or_else(|| "".to_string()); + Some(format!(" [prev_user={prev};next_user={next}]")) +} + fn request_input_shape(request: &responses::ResponsesRequest) -> String { - request - .input() - .into_iter() + let input = request.input(); + input + .iter() .enumerate() .map(|(idx, item)| { let Some(item_type) = item.get("type").and_then(Value::as_str) else { @@ -125,8 +186,9 @@ fn request_input_shape(request: &responses::ResponsesRequest) -> String { match item_type { "message" => { let role = item.get("role").and_then(Value::as_str).unwrap_or("unknown"); - let text = message_text_for_shape(&item); - format!("{idx:02}:message/{role}:{text}") + let text = message_text_for_shape(item); + let suffix = context_anchor_suffix(&input, idx, role, &text).unwrap_or_default(); + format!("{idx:02}:message/{role}:{text}{suffix}") } "function_call" => { let name = item.get("name").and_then(Value::as_str).unwrap_or("unknown"); diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap index e507b79ecc..ff81d79009 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap @@ -5,17 +5,17 @@ expression: "sectioned_request_shapes(\"Manual /compact with prior user history Scenario: Manual /compact with prior user history compacts existing history and the follow-up turn includes the compact summary plus new user message. ## Local Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/assistant:FIRST_REPLY 05:message/user: ## Local Post-Compaction History Layout -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/user: 05:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap index b21b68af5b..e10b862de5 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap @@ -5,7 +5,7 @@ expression: "sectioned_request_shapes(\"Manual /compact with no prior user turn Scenario: Manual /compact with no prior user turn behaves as a no-op and follow-up turn carries only canonical initial context plus the new user message. ## Local Post-Compaction History Layout -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=] +01:message/user: [prev_user=;next_user=] +02:message/user:> [prev_user=;next_user=] 03:message/user: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap index 4472ea863d..61fb6dda12 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap @@ -5,17 +5,17 @@ expression: "sectioned_request_shapes(\"Mid-turn continuation compaction after t Scenario: Mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary. ## Local Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:function_call/ 05:function_call_output: 06:message/user: ## Local Post-Compaction History Layout -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/user: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap index c2452c67fa..16eb0ecf14 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap @@ -5,9 +5,9 @@ expression: "sectioned_request_shapes(\"Pre-turn auto-compaction context-window Scenario: Pre-turn auto-compaction context-window failure on main: compaction request excludes the incoming user message and the turn errors. ## Local Compaction Request (Incoming User Excluded on main) -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/assistant:FIRST_REPLY 05:message/user: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap index e8bb19a4cd..1ba966469a 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap @@ -5,20 +5,20 @@ expression: "sectioned_request_shapes(\"Pre-turn auto-compaction with a context Scenario: Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded on main. ## Local Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/assistant:FIRST_REPLY 05:message/user:USER_TWO 06:message/assistant:SECOND_REPLY -07:message/user: +07:message/user: [prev_user=USER_TWO;next_user=] 08:message/user: ## Local Post-Compaction History Layout -00:message/developer: -01:message/user: -02:message/user: +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user: [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/user:USER_TWO 05:message/user: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap index 5814c71c3a..e40d8c8b29 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap @@ -5,16 +5,16 @@ expression: "sectioned_request_shapes(\"Remote manual /compact with prior user h Scenario: Remote manual /compact with prior user history compacts existing history and follow-up includes compact summary plus new user message. ## Remote Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/assistant:REMOTE_MANUAL_FIRST_REPLY ## Remote Post-Compaction History Layout 00:message/user:USER_ONE -01:message/developer: -02:message/user: -03:message/user:> +01:message/developer: [prev_user=USER_ONE;next_user=USER_TWO] +02:message/user: [prev_user=USER_ONE;next_user=USER_TWO] +03:message/user:> [prev_user=USER_ONE;next_user=USER_TWO] 04:message/user: 05:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap index 4e4c7098ca..4978a9aa74 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap @@ -5,12 +5,12 @@ expression: "sectioned_request_shapes(\"Remote manual /compact with no prior use Scenario: Remote manual /compact with no prior user turn still issues a compact request on main; follow-up turn carries canonical context and new user message. ## Remote Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=] +01:message/user: [prev_user=;next_user=] +02:message/user:> [prev_user=;next_user=] ## Remote Post-Compaction History Layout -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap index 979c122b15..9c7b22a306 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap @@ -5,16 +5,16 @@ expression: "sectioned_request_shapes(\"Remote mid-turn continuation compaction Scenario: Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary. ## Remote Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:function_call/ 05:function_call_output: ## Remote Post-Compaction History Layout 00:message/user:USER_ONE -01:message/developer: -02:message/user: -03:message/user:> +01:message/developer: [prev_user=USER_ONE;next_user=] +02:message/user: [prev_user=USER_ONE;next_user=] +03:message/user:> [prev_user=USER_ONE;next_user=] 04:message/user: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap index 2cac4940b1..daf7378fc5 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap @@ -5,8 +5,8 @@ expression: "sectioned_request_shapes(\"Remote pre-turn auto-compaction parse fa Scenario: Remote pre-turn auto-compaction parse failure on main: compaction request excludes the incoming user message and the turn stops. ## Remote Compaction Request (Incoming User Excluded on main) -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/assistant:REMOTE_FIRST_REPLY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap index 48346aebab..def3975e0d 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap @@ -5,21 +5,21 @@ expression: "sectioned_request_shapes(\"Remote pre-turn auto-compaction with a c Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message on main. ## Remote Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +00:message/developer: [prev_user=;next_user=USER_ONE] +01:message/user: [prev_user=;next_user=USER_ONE] +02:message/user:> [prev_user=;next_user=USER_ONE] 03:message/user:USER_ONE 04:message/assistant:REMOTE_FIRST_REPLY 05:message/user:USER_TWO 06:message/assistant:REMOTE_SECOND_REPLY -07:message/user: +07:message/user: [prev_user=USER_TWO;next_user=] ## Remote Post-Compaction History Layout 00:message/user:USER_ONE 01:message/user:USER_TWO 02:message/user:USER_THREE -03:message/developer: -04:message/user: -05:message/user: +03:message/developer: [prev_user=USER_THREE;next_user=USER_THREE] +04:message/user: [prev_user=USER_THREE;next_user=USER_THREE] +05:message/user: [prev_user=USER_THREE;next_user=USER_THREE] 06:message/user: 07:message/user:USER_THREE