From 217eeaee9cda828f9f67a748a3b8212b63ef526d Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Fri, 13 Mar 2026 06:39:49 -0700 Subject: [PATCH] Disambiguate snapshot cwd placeholders Co-authored-by: Codex --- .../core/tests/common/context_snapshot.rs | 158 +++++++++++++----- ...le_layout_cwd_change_refreshes_agents.snap | 8 +- 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/codex-rs/core/tests/common/context_snapshot.rs b/codex-rs/core/tests/common/context_snapshot.rs index 14977326fb..2283eb1dd9 100644 --- a/codex-rs/core/tests/common/context_snapshot.rs +++ b/codex-rs/core/tests/common/context_snapshot.rs @@ -1,4 +1,5 @@ use serde_json::Value; +use std::collections::HashMap; use crate::responses::ResponsesRequest; @@ -42,6 +43,7 @@ pub fn format_request_input_snapshot( } pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshotOptions) -> String { + let mut canonicalizer = SnapshotCanonicalizer::default(); items .iter() .enumerate() @@ -70,7 +72,11 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot .iter() .map(|entry| { if let Some(text) = entry.get("text").and_then(Value::as_str) { - return format_snapshot_text(text, options); + return format_snapshot_text( + text, + options, + &mut canonicalizer, + ); } let Some(content_type) = entry.get("type").and_then(Value::as_str) @@ -123,7 +129,7 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot let output = item .get("output") .and_then(Value::as_str) - .map(|output| format_snapshot_text(output, options)) + .map(|output| format_snapshot_text(output, options, &mut canonicalizer)) .unwrap_or_else(|| "".to_string()); format!("{idx:02}:function_call_output:{output}") } @@ -139,7 +145,9 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot .collect::>() .join(" ") }) - .map(|command| format_snapshot_text(&command, options)) + .map(|command| { + format_snapshot_text(&command, options, &mut canonicalizer) + }) .filter(|cmd| !cmd.is_empty()) .unwrap_or_else(|| "".to_string()); format!("{idx:02}:local_shell_call:{command}") @@ -151,7 +159,7 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot .and_then(|summary| summary.first()) .and_then(|entry| entry.get("text")) .and_then(Value::as_str) - .map(|text| format_snapshot_text(text, options)) + .map(|text| format_snapshot_text(text, options, &mut canonicalizer)) .unwrap_or_else(|| "".to_string()); let has_encrypted_content = item .get("encrypted_content") @@ -211,17 +219,71 @@ pub fn format_labeled_items_snapshot( format!("Scenario: {scenario}\n\n{sections}") } -fn format_snapshot_text(text: &str, options: &ContextSnapshotOptions) -> String { +#[derive(Default)] +struct SnapshotCanonicalizer { + cwd_placeholders: HashMap, +} + +impl SnapshotCanonicalizer { + fn canonicalize_text(&mut self, text: &str) -> String { + if text.starts_with("") { + return "".to_string(); + } + if text.starts_with("# AGENTS.md instructions for ") { + return "".to_string(); + } + if text.starts_with("") { + if let (Some(cwd_start), Some(cwd_end)) = (text.find(""), text.find("")) { + let cwd = &text[cwd_start + "".len()..cwd_end]; + return if cwd.ends_with("PRETURN_CONTEXT_DIFF_CWD") { + "".to_string() + } else { + let next_idx = self.cwd_placeholders.len() + 1; + let idx = *self + .cwd_placeholders + .entry(cwd.to_string()) + .or_insert(next_idx); + format!(">") + }; + } + return "".to_string(); + } + if text.starts_with("") { + let subagent_count = text + .lines() + .filter(|line| line.trim_start().starts_with("- ")) + .count(); + return format!(""); + } + if text.starts_with("You are performing a CONTEXT CHECKPOINT COMPACTION.") { + return "".to_string(); + } + if text.starts_with("Another language model started to solve this problem") + && let Some((_, summary)) = text.split_once('\n') + { + return format!("\n{summary}"); + } + text.to_string() + } +} + +fn format_snapshot_text( + text: &str, + options: &ContextSnapshotOptions, + canonicalizer: &mut SnapshotCanonicalizer, +) -> String { match options.render_mode { ContextSnapshotRenderMode::RedactedText => { - normalize_snapshot_line_endings(&canonicalize_snapshot_text(text)).replace('\n', "\\n") + normalize_snapshot_line_endings(&canonicalizer.canonicalize_text(text)) + .replace('\n', "\\n") } ContextSnapshotRenderMode::FullText => { normalize_snapshot_line_endings(text).replace('\n', "\\n") } ContextSnapshotRenderMode::KindWithTextPrefix { max_chars } => { - let normalized = normalize_snapshot_line_endings(&canonicalize_snapshot_text(text)) - .replace('\n', "\\n"); + let normalized = + normalize_snapshot_line_endings(&canonicalizer.canonicalize_text(text)) + .replace('\n', "\\n"); if normalized.chars().count() <= max_chars { normalized } else { @@ -237,42 +299,6 @@ fn normalize_snapshot_line_endings(text: &str) -> String { text.replace("\r\n", "\n").replace('\r', "\n") } -fn canonicalize_snapshot_text(text: &str) -> String { - if text.starts_with("") { - return "".to_string(); - } - if text.starts_with("# AGENTS.md instructions for ") { - return "".to_string(); - } - if text.starts_with("") { - if let (Some(cwd_start), Some(cwd_end)) = (text.find(""), text.find("")) { - let cwd = &text[cwd_start + "".len()..cwd_end]; - return if cwd.ends_with("PRETURN_CONTEXT_DIFF_CWD") { - "".to_string() - } else { - ">".to_string() - }; - } - return "".to_string(); - } - if text.starts_with("") { - let subagent_count = text - .lines() - .filter(|line| line.trim_start().starts_with("- ")) - .count(); - return format!(""); - } - if text.starts_with("You are performing a CONTEXT CHECKPOINT COMPACTION.") { - return "".to_string(); - } - if text.starts_with("Another language model started to solve this problem") - && let Some((_, summary)) = text.split_once('\n') - { - return format!("\n{summary}"); - } - text.to_string() -} - #[cfg(test)] mod tests { use super::ContextSnapshotOptions; @@ -360,6 +386,50 @@ mod tests { assert_eq!(rendered, "00:message/developer:"); } + #[test] + fn redacted_text_mode_assigns_distinct_cwd_placeholders_within_one_snapshot() { + let items = vec![ + json!({ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "\n /tmp/one\n" + }] + }), + json!({ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "\n /tmp/two\n" + }] + }), + json!({ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "\n /tmp/one\n" + }] + }), + ]; + + let rendered = format_response_items_snapshot( + &items, + &ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText), + ); + + assert_eq!( + rendered, + concat!( + "00:message/user:>\n", + "01:message/user:>\n", + "02:message/user:>" + ) + ); + } + #[test] fn kind_with_text_prefix_mode_normalizes_crlf_line_endings() { let items = vec![json!({ diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_refreshes_agents.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_refreshes_agents.snap index 8e888da88e..43d42688cd 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_refreshes_agents.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_refreshes_agents.snap @@ -8,19 +8,19 @@ Scenario: Second turn changes cwd to a directory with different AGENTS.md; updat 00:message/developer: 01:message/user[2]: [01] - [02] > + [02] > 02:message/user:first turn in agents_one ## Second Request (agents_two cwd) 00:message/developer: 01:message/user[2]: [01] - [02] > + [02] > 02:message/developer: -03:message/user:> +03:message/user:> 04:message/user:first turn in agents_one 05:message/assistant:turn one complete 06:message/user[2]: [01] - [02] > + [02] > 07:message/user:second turn in agents_two