Compare commits

...

1 Commits

Author SHA1 Message Date
Roy Han
71f1c6ed79 persist untruncated approve rule list 2026-03-17 17:22:53 -07:00
17 changed files with 120 additions and 21 deletions

View File

@@ -85,6 +85,7 @@ pub fn create_fake_rollout_with_source(
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
};
let payload = serde_json::to_value(SessionMetaLine {
meta,
@@ -167,6 +168,7 @@ pub fn create_fake_rollout_with_text_elements(
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
};
let payload = serde_json::to_value(SessionMetaLine {
meta,

View File

@@ -328,6 +328,7 @@ stream_max_retries = 0
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
};
std::fs::write(
&rollout_path,

View File

@@ -71,6 +71,7 @@ mod tests {
request_max_retries: None,
stream_max_retries: None,
stream_idle_timeout_ms: None,
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
};

View File

@@ -91,9 +91,11 @@ use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::models::summarize_allow_prefixes;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::ApprovedCommandPrefixesSnapshot;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::HasLegacyEvent;
use codex_protocol::protocol::ItemCompletedEvent;
@@ -1404,6 +1406,9 @@ impl Session {
let (conversation_id, rollout_params) = match &initial_history {
InitialHistory::New | InitialHistory::Forked(_) => {
let conversation_id = ThreadId::default();
let approved_command_prefixes = exec_policy.current().get_allowed_prefixes();
let allow_prefix_summary =
summarize_allow_prefixes(approved_command_prefixes.clone());
(
conversation_id,
RolloutRecorderParams::new(
@@ -1414,6 +1419,12 @@ impl Session {
text: session_configuration.base_instructions.clone(),
},
session_configuration.dynamic_tools.clone(),
ApprovedCommandPrefixesSnapshot {
prefixes: approved_command_prefixes,
prompt_truncated: allow_prefix_summary.prompt_truncated,
total_count: allow_prefix_summary.total_count,
prompt_visible_count: allow_prefix_summary.prompt_visible_count,
},
if session_configuration.persist_extended_history {
EventPersistenceMode::Extended
} else {

View File

@@ -2008,6 +2008,7 @@ async fn attach_rollout_recorder(session: &Arc<Session>) -> PathBuf {
SessionSource::Exec,
BaseInstructions::default(),
Vec::new(),
Default::default(),
EventPersistenceMode::Limited,
),
None,
@@ -3902,6 +3903,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
SessionSource::Exec,
BaseInstructions::default(),
Vec::new(),
Default::default(),
EventPersistenceMode::Limited,
),
None,
@@ -3999,6 +4001,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
SessionSource::Exec,
BaseInstructions::default(),
Vec::new(),
Default::default(),
EventPersistenceMode::Limited,
),
None,

View File

@@ -44,6 +44,7 @@ async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
};

View File

@@ -44,6 +44,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
};
let session_meta_line = SessionMetaLine {
meta: session_meta,
@@ -94,9 +95,11 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
};
let polluted_meta = SessionMeta {
memory_mode: Some("polluted".to_string()),
approved_command_prefixes: None,
..session_meta.clone()
};
let lines = vec![
@@ -361,6 +364,7 @@ fn write_rollout_in_sessions_with_cwd(
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
};
let session_meta_line = SessionMetaLine {
meta: session_meta,

View File

@@ -47,6 +47,7 @@ use crate::state_db;
use crate::state_db::StateDbHandle;
use crate::truncate::TruncationPolicy;
use crate::truncate::truncate_text;
use codex_protocol::protocol::ApprovedCommandPrefixesSnapshot;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::ResumedHistory;
@@ -83,6 +84,7 @@ pub enum RolloutRecorderParams {
source: SessionSource,
base_instructions: BaseInstructions,
dynamic_tools: Vec<DynamicToolSpec>,
approved_command_prefixes: ApprovedCommandPrefixesSnapshot,
event_persistence_mode: EventPersistenceMode,
},
Resume {
@@ -112,6 +114,7 @@ impl RolloutRecorderParams {
source: SessionSource,
base_instructions: BaseInstructions,
dynamic_tools: Vec<DynamicToolSpec>,
approved_command_prefixes: ApprovedCommandPrefixesSnapshot,
event_persistence_mode: EventPersistenceMode,
) -> Self {
Self::Create {
@@ -120,6 +123,7 @@ impl RolloutRecorderParams {
source,
base_instructions,
dynamic_tools,
approved_command_prefixes,
event_persistence_mode,
}
}
@@ -381,6 +385,7 @@ impl RolloutRecorder {
source,
base_instructions,
dynamic_tools,
approved_command_prefixes,
event_persistence_mode,
} => {
let log_file_info = precompute_log_file_info(config, conversation_id)?;
@@ -415,6 +420,7 @@ impl RolloutRecorder {
},
memory_mode: (!config.memories.generate_memories)
.then_some("disabled".to_string()),
approved_command_prefixes: Some(approved_command_prefixes),
};
(

View File

@@ -4,6 +4,7 @@ use crate::features::Feature;
use chrono::TimeZone;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::ApprovedCommandPrefixesSnapshot;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::SandboxPolicy;
@@ -59,6 +60,15 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
.build()
.await?;
let thread_id = ThreadId::new();
let approved_command_prefixes = ApprovedCommandPrefixesSnapshot {
prefixes: vec![
vec!["cargo".to_string(), "test".to_string()],
vec!["git".to_string(), "fetch".to_string()],
],
prompt_truncated: true,
total_count: 101,
prompt_visible_count: 100,
};
let recorder = RolloutRecorder::new(
&config,
RolloutRecorderParams::new(
@@ -67,6 +77,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
SessionSource::Exec,
BaseInstructions::default(),
Vec::new(),
approved_command_prefixes.clone(),
EventPersistenceMode::Limited,
),
None,
@@ -120,6 +131,10 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
text.contains("\"type\":\"session_meta\""),
"expected session metadata in rollout"
);
assert!(
text.contains(&serde_json::to_string(&approved_command_prefixes)?),
"expected approved command prefixes snapshot in rollout"
);
let buffered_idx = text
.find("buffered-event")
.expect("buffered event in rollout");
@@ -166,6 +181,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu
SessionSource::Cli,
BaseInstructions::default(),
Vec::new(),
Default::default(),
EventPersistenceMode::Limited,
),
Some(state_db.clone()),

View File

@@ -1107,6 +1107,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
}),

View File

@@ -72,6 +72,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
};
@@ -116,6 +117,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
};

View File

@@ -172,6 +172,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
SessionSource::Exec,
BaseInstructions::default(),
Vec::new(),
Default::default(),
EventPersistenceMode::Limited,
),
None,

View File

@@ -147,6 +147,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
base_instructions: None,
dynamic_tools: Some(dynamic_tools_for_hook),
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
};

View File

@@ -778,13 +778,18 @@ const MAX_RENDERED_PREFIXES: usize = 100;
const MAX_ALLOW_PREFIX_TEXT_BYTES: usize = 5000;
const TRUNCATED_MARKER: &str = "...\n[Some commands were truncated]";
pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
let mut truncated = false;
if prefixes.len() > MAX_RENDERED_PREFIXES {
truncated = true;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllowPrefixRenderSummary {
pub rendered: String,
pub prompt_truncated: bool,
pub total_count: usize,
pub prompt_visible_count: usize,
}
pub fn summarize_allow_prefixes(mut prefixes: Vec<Vec<String>>) -> AllowPrefixRenderSummary {
let total_count = prefixes.len();
let mut truncated = total_count > MAX_RENDERED_PREFIXES;
let mut prefixes = prefixes;
prefixes.sort_by(|a, b| {
a.len()
.cmp(&b.len())
@@ -792,15 +797,15 @@ pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
.then_with(|| a.cmp(b))
});
let full_text = prefixes
let rendered_prefixes = prefixes
.into_iter()
.take(MAX_RENDERED_PREFIXES)
.map(|prefix| format!("- {}", render_command_prefix(&prefix)))
.collect::<Vec<_>>()
.join("\n");
.collect::<Vec<_>>();
let mut output = rendered_prefixes.join("\n");
let mut prompt_visible_count = rendered_prefixes.len();
// truncate to last UTF8 char
let mut output = full_text;
let byte_idx = output
.char_indices()
.nth(MAX_ALLOW_PREFIX_TEXT_BYTES)
@@ -808,13 +813,27 @@ pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
if let Some(byte_idx) = byte_idx {
truncated = true;
output = output[..byte_idx].to_string();
prompt_visible_count = if output.is_empty() {
0
} else {
output.lines().count()
};
}
if truncated {
Some(format!("{output}{TRUNCATED_MARKER}"))
} else {
Some(output)
output = format!("{output}{TRUNCATED_MARKER}");
}
AllowPrefixRenderSummary {
rendered: output,
prompt_truncated: truncated,
total_count,
prompt_visible_count,
}
}
pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
Some(summarize_allow_prefixes(prefixes).rendered)
}
fn prefix_combined_str_len(prefix: &[String]) -> usize {
@@ -2280,10 +2299,11 @@ mod tests {
.map(|i| vec![format!("{i:03}")])
.collect::<Vec<_>>();
let output = format_allow_prefixes(prefixes).expect("rendered list");
assert_eq!(output.ends_with(TRUNCATED_MARKER), true);
eprintln!("output: {output}");
assert_eq!(output.lines().count(), MAX_RENDERED_PREFIXES + 1);
let summary = summarize_allow_prefixes(prefixes);
assert!(summary.rendered.ends_with(TRUNCATED_MARKER));
assert_eq!(summary.total_count, MAX_RENDERED_PREFIXES + 5);
assert_eq!(summary.prompt_visible_count, MAX_RENDERED_PREFIXES);
assert_eq!(summary.rendered.lines().count(), MAX_RENDERED_PREFIXES + 1);
}
#[test]
@@ -2298,12 +2318,26 @@ mod tests {
.expect("add rule");
}
let output =
format_allow_prefixes(exec_policy.get_allowed_prefixes()).expect("formatted prefixes");
let summary = summarize_allow_prefixes(exec_policy.get_allowed_prefixes());
assert!(
output.len() <= MAX_ALLOW_PREFIX_TEXT_BYTES + TRUNCATED_MARKER.len(),
"output length exceeds expected limit: {output}",
summary.rendered.len() <= MAX_ALLOW_PREFIX_TEXT_BYTES + TRUNCATED_MARKER.len(),
"output length exceeds expected limit: {}",
summary.rendered,
);
assert!(summary.prompt_truncated);
assert!(summary.prompt_visible_count < summary.total_count);
}
#[test]
fn summarize_allow_prefixes_reports_untruncated_counts() {
let summary = summarize_allow_prefixes(vec![
vec!["git".to_string(), "fetch".to_string()],
vec!["cargo".to_string(), "test".to_string()],
]);
assert_eq!(summary.total_count, 2);
assert_eq!(summary.prompt_visible_count, 2);
assert!(!summary.prompt_truncated);
}
#[test]

View File

@@ -2344,6 +2344,14 @@ impl fmt::Display for SubAgentSource {
/// NOTE: There used to be an `instructions` field here, which stored user_instructions, but we
/// now save that on TurnContext. base_instructions stores the base instructions for the session,
/// and should be used when there is no config override.
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
pub struct ApprovedCommandPrefixesSnapshot {
pub prefixes: Vec<Vec<String>>,
pub prompt_truncated: bool,
pub total_count: usize,
pub prompt_visible_count: usize,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct SessionMeta {
pub id: ThreadId,
@@ -2370,6 +2378,8 @@ pub struct SessionMeta {
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved_command_prefixes: Option<ApprovedCommandPrefixesSnapshot>,
}
impl Default for SessionMeta {
@@ -2388,6 +2398,7 @@ impl Default for SessionMeta {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
}
}
}

View File

@@ -257,6 +257,7 @@ mod tests {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
}),
@@ -383,6 +384,7 @@ mod tests {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: None,
}),

View File

@@ -764,6 +764,7 @@ mod tests {
base_instructions: None,
dynamic_tools: None,
memory_mode: Some("polluted".to_string()),
approved_command_prefixes: None,
},
git: None,
})];
@@ -818,6 +819,7 @@ mod tests {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
},
git: Some(GitInfo {
commit_hash: Some("rollout-sha".to_string()),